函数(下)
【函数(下)】
用new操作符调用函数
现在,我们学习一种新的函数调用方式:new 函数()
你可能知道 new 操作符和 “面向对象” 息息相关,但是现在我们先不探讨它的 “面向对象” 意义,而是先把用 new 调用函数的执行步骤和它上下文弄清楚。
用new调用函数的四步走
JS 规定,使用 new 操作符调用函数会进行 “四步走”:
- 函数体内会自动创建出一个空白对象
- 函数的上下文(this)会指向这个对象
- 函数体内的语句会执行
- 函数会自动返回上下文对象,即使函数没有 return 语句
四步走详解
1 | function fun() { |
【第一步:函数体内会自动创建出一个空白对象】
【第二步:函数的上下文(this)会指向这个对象】
【第三步:执行函数体中的语句】
之后这个对象就不再是空对象了。
【第四步:函数会自动返回上下文对象,即使函数没有 return 语句】
执行结果为:{a: 3, b: 5}
【案例】
1 | function fun() { |
上下文规则总结
规则 | 上下文 |
---|---|
对象.函数() |
对象 |
函数() |
window |
数组[下标]() |
数组 |
IIFE |
window |
定时器 |
window |
DOM事件处理函数 |
绑定 DOM 的元素 |
call和apply |
任意指定 |
用new调用函数 |
秘密创建出的对象 |
构造函数
什么是构造函数
构造函数是专门用来创建对象的函数
一个构造函数我们也可以称为一个类
通过一个构造函数创建的对象,我们称该对象时这个构造函数的实例
通过同一个构造函数创建的对象,我们称为一类对象
构造函数就是一个普通的函数,只是他的调用方式不同,
如果直接调用,它就是一个普通函数
如果使用new来调用,则它就是一个构造函数
我们将之前书写的函数进行一下小改进:
1 | // 书写规范:构造函数首字母大写 |
- 用 new 调用一个函数,这个函数就被称为 “构造函数”,任何函数都可以是构造函数,只需要用 new 调用它
- 顾名思义,构造函数用来 “构造新对象”,它内部的语句将为新对象添加若干属性和方法,完成对象的初始化
- 构造函数必须用 new 关键字调用,否则不能正常工作,正因如此,开发者约定构造函数命名时首字母要大写
注意:一个函数是不是构造函数,要看它是否用 new 调用,而至于名称首字母大写,完全是开发者的习惯约定。
如果不用new调用构造函数
1 | function People(name, age, sex) { |
为对象添加方法
1 | function People(name, age, sex) { |
注意:直接将方法写在构造函数中的方式是不妥的,后面会讲解原因。
类与实例
基本介绍
【类好比是 “蓝图”】
如同 “蓝图” 一样,类只描述对象会拥有哪些属性和方法,但是并不具体指明属性的值。
【实例是具体的对象】
【构造函数和 “类”】
- Java、C++ 等是 “面向对象” 语言
- JavaScript 是 “基于对象” 语言
- JavaScript 中的构造函数可以类比于 OO 语言中的 “类”,写法的确类似,但和真正 OO语言 还是有本质不同,在后续课程还将看见 JS 和其他 OO 语言完全不同的、特有的原型特性。
JS 构造函数 ≈ OO 语言 “类”
JS 构造函数可以看做是面向对象语言中的 “类”
实例成员和静态成员
new
出来的实例对象就是构造函数内部的this
实例成员就是this
后面的属性和方法
1 | <script type="text/javascript"> |
- 实例成员的this指向实例对象
- 静态成员的this指向该构造函数本身
原型(prototype)
什么是原型
JavaScript 原型是每个对象都具有的属性,它指向一个对象,我们通常称之为原型对象。
当我们访问一个对象的属性或方法时,JavaScript 引擎首先会在该对象自身查找,如果找到则直接使用。如果找不到,则会去该对象的原型对象中查找。
任何函数都有 prototype 属性,prototype 是英语 “原型” 的意思。
prototype 属性值是个对象, 每个原型对象里面都有个constructor 属性(constructor 构造函数)
作用:该属性指向该原型对象的构造函数,
constructor:制造商
1 | function sum(a, b) { |
对于普通函数来说的 prototype 属性没有任何用处,而构造函数的 prototype 属性非常有用。
构造函数的 prototype 属性是它的实例的原型。
所有的 JavaScript 对象都会从一个 prototype(原型对象)中继承属性和方法:
Date
对象从Date.prototype
继承。Array
对象从Array.prototype
继承。Person
对象从Person.prototype
继承。
所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。
JavaScript 对象有一个指向一个原型对象的链。
创建一个函数以后,解析器都会默认在函数中添加一个数prototype
prototype属性指向的是一个对象,这个对象我们称为原型对象。
构造函数的prototype是实例的原型
实例对象都会有一个属性 proto(对象原型) 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。
1 | 这个隐含的属性可以通过对象.__proto__来访问。 |
原型对象就相当于一个公共的区域,凡是通过同一个构造函数创建的对象他们通常都可以访问到相同的原型对象。
我们可以将对象中共有的属性和方法统一添加到原型对象中,
这样我们只需要添加一次,就可以使所有的对象都可以使用。
People.prototype
是 xiaoming
的原型。
1 | function People(name, age, sex) { |
补充一张关于原型的图
原型链查找
JavaScript 规定:实例可以 “打点” 访问它的原型的属性和方法,这被称为 “原型链查找”。
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
1 | function People(name, age, sex) { |
【遮蔽效应】
1 | function People(name, age, sex) { |
hasOwnProperty
hasOwnProperty 方法可以检查对象是否真正 “自己拥有” 某属性或者方法。
1 | xiaoming.hasOwnProperty('name'); // true |
in
in 运算符只能检查某个属性或方法是否可以被对象访问,不能检查是否是自己的属性或方法。
1 | 'name' in xiaoming // true |
原型-添加属性和方法
为什么要写在原型上
在之前的课程中,我们把方法都是直接添加到实例身上:
1 | function People(name, age, sex) { |
把方法直接添加到实例身上的缺点:每个实例和每个实例的方法函数都是内存中不同的函数,造成了内存的浪费。
解决办法:将方法写到 prototype 上。
方法要写到 prototype 上
使用 prototype 属性就可以给对象的构造函数添加新的属性和方法。
1 | function People(name, age, sex) { |
原型对象里面放的是方法, 这个方法里面的this 指向的是这个方法的调用者, 也就是这个实例对象。
原型链
基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对 象的链状结构关系称为原型链.
Object 可以看做是所有对象的构造函数。
所以,People.prototype 这个对象可以看做是 Object new 出来的。
1 | function People() { |
查找规则
① 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
② 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)
③ 如果还没有就查找原型对象的原型(Object的原型对象)
④ 依此类推一直找到 Object 为止(null)
⑤ __proto__对象原型的意义就在于为实例对象查找机制提供一个方向,或者说一条路线
【关于数组的原型链】
任何数组实际上都是可以看做是 Array 这个构造函数 new 出来的。
包装类
定义
-
在JS中为我们提供了三个包装类:
String() Boolean() Number()
通过这三个包装类可以创建基本数据类型的对象 -
很多编程语言都有 “包装类” 的设计,包装类的目的就是为了让基本类型值可以从它们的构造函数的 prototype 上获得方法
-
例子:
1
2
3var num = new Number(2);
var str = new String("hello");
var bool = new Boolean(true);Number
、String
和Boolean
这三个原生对象,如果不作为构造函数调用(即调用时不加new
),而是作为普通函数调用,常常用于将任意类型的值转为数值、字符串和布尔值。总结一下,这三个对象作为构造函数使用(带有
new
)时,可以将原始类型的值转为对象;作为普通函数使用时(不带有new
),可以将任意类型的值,转为原始类型的值。在实际应用中千万不要这么干。
某些场合,原始类型的值会自动当作包装对象调用,即调用包装对象的属性和方法。这时,JavaScript 引擎会自动将原始类型的值转为包装对象实例,并在使用后立刻销毁实例。
比如,字符串可以调用
length
属性,返回字符串的长度。1
'abc'.length // 3
上面代码中,
abc
是一个字符串,本身不是对象,不能调用length
属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用length
属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换。1
2
3
4
5
6
7
8
9var str = 'abc';
str.length // 3
// 等同于
var strObj = new String(str)
// String {
// 0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"
// }
strObj.length // 3上面代码中,字符串
abc
的包装对象提供了多个属性,length
只是其中之一。自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性。
1
2
3var s = 'Hello World';
s.x = 123;
s.x // undefined上面代码为字符串
s
添加了一个x
属性,结果无效,总是返回undefined
。另一方面,调用结束后,包装对象实例会自动销毁。这意味着,下一次调用字符串的属性时,实际是调用一个新生成的对象,而不是上一次调用时生成的那个对象,所以取不到赋值在上一个对象的属性。如果要为字符串添加属性,只有在它的原型对象
String.prototype
上定义(参见《面向对象编程》章节)。
举例
请看下面的程序:
1 | var a = new Number(123); |
实例方法
三种包装对象各自提供了许多实例方法,详见后文。这里介绍两种它们共同具有、从Object
对象继承的方法:valueOf()
和toString()
。
valueOf()
valueOf()
方法返回包装对象实例对应的原始类型的值。
1 | new Number(123).valueOf() // 123 |
toString()
toString()
方法返回对应的字符串形式。
1 | new Number(123).toString() // "123" |
自定义方法
除了原生的实例方法,包装对象还可以自定义方法和属性,供原始类型的值直接调用。
比如,我们可以新增一个double
方法,使得字符串和数字翻倍。
1 | String.prototype.double = function () { |
上面代码在String
和Number
这两个对象的原型上面,分别自定义了一个方法,从而可以在所有实例对象上调用。注意,最后一行的123
外面必须要加上圆括号,否则后面的点运算符(.
)会被解释成小数点。
总结
- Number()、String() 和 Boolean() 的实例都是 object 类型,它们的 PrimitiveValue 属性存储它们的本身值
- new 出来的基本类型值可以正常参与运算
- 包装类的目的就是为了让基本类型值可以从它们的构造函数的 prototype 上获得方法(打点调用)
1
2
3
4
5 var d = 123;
console.log(d.__proto__ == Number.prototype); // true
var e = '慕课网';
console.log(e.__proto__ == String.prototype); // true从以上代码可以看出,直接定义的基本变量本质也是 new 出来的,所以才可以直接打点调用相关方法。
注意:只有 Number()、String()、Boolean() 才是包装类, 而 Array() 不是数组的包装类,因为数组不是基本类型谈不上 “包装类” 这一说法的。
垃圾回收
-
什么是垃圾回收机制?
垃圾回收机制(Garbage Collection) 简称 GC JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况
(不再用到的内存,没有及时释放,就叫做内存泄漏) -
内存的生命周期
JS环境中分配的内存, 一般有如下生命周期:
-
内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
-
内存使用:即读写内存,也就是使用变量、函数等
-
内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
说明: 全局变量一般不会回收(关闭页面回收); 一般情况下局部变量的值, 不用了, 会被自动回收掉
-
就像人生活的时间长了会产生垃圾一样,程序运行过程中也会产生垃圾
这些垃圾积攒过多以后,会导致程序运行的速度过慢,
所以我们需要一个垃圾回收的机制,来处理程序运行过程中产生垃圾
当一个对象没有任何的变量或属性对它进行引用,此时我们将永远无法操作该对象,
此时这种对象就是一个垃圾,这种对象过多会占用大量的内存空间,导致程序运行变慢,
所以这种垃圾必须进行清理。
在JS中拥有自动的垃圾回收机制,会自动将这些垃圾对象从内存中销毁,
我们不需要也不能进行垃圾回收的操作
我们需要做的只是要将不再使在使用的变量设置为null
高阶函数-回调函数
1. 函数表达式 ,函数也是【数据】,把函数赋值给变量
- 回调函数 , 把函数当做另外一个函数的参数传递,这个函数就叫回调函数 。回调函数本质还是函数,只不过把它当成参数使用 , 使用匿名函数做为回调函数比较常见。
如果将函数 A 做为参数传递给函数 B 时,我们称函数 A 为回调函数。
1 | <script> |
函数 bar
做参数传给了 foo
函数,bar
就是所谓的回调函数了!!!
我们回顾一下间歇函数 setInterval
1 | <script> |
fn
函数做为参数传给了 setInterval
,这便是回调函数的实际应用了,结合刚刚学习的函数表达式上述代码还有另一种更常见写法。
1 | <script> |
结论:
- 回调函数本质还是函数,只不过把它当成参数使用
- 使用匿名函数做为回调函数比较常见
高阶函数-闭包
1 | // 定义一个全局变量 |
什么是闭包
闭包是函数本身和该函数声明时所处的环境状态的组合。
函数能够 “记忆” 其定义时所处的环境,即使函数不在其定义的环境中被调用,也能访问定义时所处环境的变量。
在 JS 中,每次创建函数时都会创建闭包。
但是,闭包特性往往需要将函数 “换一个地方” 执行,才能被直观的体现出来。
闭包很有用,因为它允许我们将数据与操作该数据的函数关联起来,这与 “面向对象编程” 有少许相似之处。
闭包的功能:记忆性、模拟私有变量(相当于把函数的数据封装了)。
闭包常见写法
1 | 简单的写法 |
闭包用途 - 记忆性
当闭包产生时,函数所处环境的状态会始终保持在内存中,不会在外层函数调用后自动清除。这就是闭包的记忆性。
【闭包的记忆性举例】
创建体温检测函数 checkTemp(n),可以检查体温 n 是否正常,函数会返回布尔值。
但是,不同的小区有不同的体温检测标准,比如 A 小区体温合格线是 37.1℃,而 B 小区体温合格线是 37.3℃,应该怎么编程呢?
1 | function createCheckTemp(standardTemp) { |
计数器案例
1 | // 闭包的应用 |
闭包有点像c语言的静态属性
使用闭包的注意点
不能滥用闭包!否则会造成网页的性能问题,严重时可能导致 “内存泄漏”。
所谓 “内存泄漏” 就是指程序中已经动态分配的内存由于某种原因未释放或无法释放。
目前,Chrome 等比较先进的浏览器很少发生内存泄漏。
闭包面试题
1 | function addCount() { |
立即执行函数 IIFE
形成 IIFE 的方法
IIFE 立即调用函数表达式,是一种特殊的 JS 函数写法,函数定义完,立即被调用,这种函数叫做立即执行函数。
立即执行函数往往只会执行一次
蓝色括号里写传递的参数。
常用
()
来将函数转为 “函数表达式”。
1 | <script type="text/javascript"> |
IIFE的作用1 - 为变量赋值
1 | var age = 12; |
IIFE的作用2 - 将全局变量变为局部变量
先看一个问题:
1 | var arr = []; |
IIFE 可以在一些场合(如 for 循环中)将全局变量变为局部变量,语法显得紧凑。
在 ES6 中,有更好的方式可以约定变量的作用域
1 | var arr = []; |