视频教程:https://www.bilibili.com/video/BV1Kt411w7MP?from=search&seid=10263099956041583084
ES5就是指 ECMAScript5 (2009),下文将介绍在2009年ES5中的发布的新特性。
ES6之前的面向对象
构造函数和原型
在典型的 OOP 的语言中(如 Java),都存在类的概念,类就是对象的模板,对象就是类的实例,但在 ES6之前, JS 中并没用引入类的概念。
ES6, 全称 ECMAScript 6.0 ,2015.06 发版。但是目前浏览器的 JavaScript 是 ES5 版本,大多数高版本的浏览器也支持 ES6,不过只实现了 ES6 的部分特性和功能。
在 ES6之前 ,对象不是基于类创建的,而是用一种称为构建函数的特殊函数来定义对象和它们的特征。
创建对象的三种方式:
- 对象字面量
- new Object()
- 自定义的构造函数
构造函数
这是一种特殊的函数,主要用来初始化对象,即为对象的成员变量赋初始值,与new一起搭配使用。
使用构造函数时要注意两点:
- 构造函数用于创建某一类对象时,其函数名的首字母要大写
- 构造函数要和new一起使用
使用构造函数创建一个对象的过程叫做对象实例化,因此对象也称为实例。
var qq = new Persion('qq', 18);
这行代码的执行过程:
① 首先在内存中创建了一个空的对象(属于Person类)
② 此时函数内的this指向这个对象
③ 函数内的代码依次执行,给这个对象添加属性和方法
④ 函数执行完毕,返回这个对象给qq
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHi = function(words) {
console.log(words);
};
}
var qq = new Person('qq', 18);
console.log(qq.name, qq.age);
qq.sayHi("hi~");
存在的问题:
构造函数方法会存在浪费内存的问题,因为每次我们使用new 构造函数()
创建一个新的实例时,都会在内存中开辟一块新的区域,用来存放实例的属性和方法,这就导致如果我们创建了很多实例,就会占据大量内存。
为解决这一问题,就引出了原型prototype
这一对象。
静态成员、实例成员
JavaScript 的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的 this 上添加。通过这两种方式添加的成员,就分别称为静态成员和实例成员。
- 静态成员:在构造函数本上添加的成员称为静态成员,只能由构造函数本身来访问
Person.qwe
- 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问
ldh.qwe
// 构造函数中的属性和方法我们称为成员, 成员可以添加
function Star(uname, age) {
this.uname = uname;
this.age = age;
this.sing = function() {
console.log('我会唱歌');
}
}
var ldh = new Star('刘德华', 18);
// 1.实例成员就是构造函数内部通过this添加的成员 uname age sing 就是实例成员
// 实例成员只能通过实例化的对象来访问
console.log(ldh.uname);
ldh.sing();
// console.log(Star.uname); // 不可以通过构造函数来访问实例成员
// 2. 静态成员 在构造函数本身上添加的成员 sex 就是静态成员
Star.sex = '男';
// 静态成员只能通过构造函数来访问
console.log(Star.sex);
console.log(ldh.sex); // 不能通过对象来访问
原型对象prototype和对象原型__proto__
需要注意的是,我们尽量避免在ES5的构造函数语法中,使用箭头函数来定义方法,具体原因见博客 JavaScript ES6的箭头函数 章节
先总结:
假设有一个构造函数Test
,一个实例var t1 = new Test()
① 原型对象Test.prototype
:包括 构造函数constructor、共享方法、__proto__
(这个对象原型指向上一级)
② 对象实例及内部的对象原型t1.__proto__
:console.log(t1)
中包括
- 该对象的属性、方法(构造函数内定义的)
__proto__
(这个对象原型就是构造函数的原型对象)
构造函数通过原型分配的函数是所有对象实例所共享的。
JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象。
prototype就是一个对象,它的所有属性和方法都会被构造函数拥有。
因此,我们可以把那些不变的方法,直接定义在prototype对象上,让所有对象的实例共享这些方法。
原型就是prototype,也叫做原型对象,它的作用是共享方法。
每一个对象都会有一个属性__proto__
,它指向构造函数的prototype
原型对象。
对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__
原型的存在。
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
// 使用原型对象创建共享方法
Star.prototype.sing = function() {
console.log('我会唱歌');
}
var ldh = new Star('刘德华', 18);
var zxy = new Star('张学友', 19);
ldh.sing();
console.log(ldh); // 在对象身上,系统会添加一个 __proto__ 指向该实例的构造函数的原型对象 prototype
console.log(ldh.__proto__ === Star.prototype); // true,两者相等
方法的查找规则:
- 首先先看 实例对象 身上是否有 该方法,如果有就执行这个对象上的这个方法
- 如果对象本身没有这个方法(没在构造函数中直接创建方法),因为有
__proto__
的存在,就去构造函数原型对象prototype上去查找这个方法,相当于__proto__
是为实例查找方法提供了方向
注意
__proto__
对象原型 和 prototype原型对象是等价的__proto__
对象原型的意义在于为对象的查找机制提供一个方向,但它是一个非标准属性,实际开发中不可以使用这个属性,它只是内部指向原型对象prototype
constructor 构造函数
对象原型( __proto__)
和构造函数(prototype)原型对象
里面都有一个属性 constructor
,constructor 我们称为构造函数,因为它指回构造函数本身。
constructor 主要用于记录该对象引用自哪个构造函数,它可以让原型对象重新指向原来的构造函数。
一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
如下示例:
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
// 通过原型对象,添加共享方法的传统方式
// Star.prototype.sing = function() {
// console.log('我会唱歌');
// };
// Star.prototype.movie = function() {
// console.log('我会演电影');
// }
// 对构造函数通过原型对象添加共享方法时,可以采用对象赋值的方式来添加,更加清晰明了
Star.prototype = {
// 如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数
constructor: Star,
sing: function() {
console.log('我会唱歌');
},
movie: function() {
console.log('我会演电影');
}
}
原型链
层级关系:
ldh对象实例的原型–>Star原型对象的原型–>Object原型对象的原型–>null
对象的成员查找机制
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
- 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)。
- 如果还没有就查找原型对象的原型(Object的原型对象)。
- 依此类推一直找到 Object 为止(null)。
__proto__
对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。
原型对象 this的指向
- 构造函数中的this:指向 实例对象
- 构造函数的原型对象的共享方法中的this:也指向这个方法的调用者,即 实例对象
扩展内置对象
可以通过内助对象的原型对象,来对其进行自定义方法的扩展,比如给array数组增加自定义的求和功能。
但是,数组和字符串内置对象不能给原型对象覆盖操作 Array.prototype = {}
,只能是 Array.prototype.xxx = function(){}
的方式。
// 扩展array内置对象
Array.prototype.sum = function() {
var sum = 0;
for (var i = 0; i < this.length; i++) {
sum += this[i];
}
return sum;
};
// 这样直接赋值的扩展方法是不可用的
// Array.prototype = {
// sum: function() {
// var sum = 0;
// for (var i = 0; i < this.length; i++) {
// sum += this[i];
// }
// return sum;
// }
// }
var arr = [1, 2, 3];
console.log(arr.sum()); // 6
console.log(Array.prototype); // 除了有数组的本身自带方法外,还多了 sum()自定义方法
var arr1 = new Array(11, 22, 33);
console.log(arr1.sum()); // 66
继承
在ES6之前,我们没有extends
来进行继承,但是可以通过构造函数 + 原型对象模型实现继承,这被称为组合继承。
call 方法
这个方法可以用来调用函数,并修改函数运行时的this指向
fn.call(thisArg, arg1, arg2, ...)
参数:
- thisArg:当前调用函数在运行时的this指向对象
- arg1,arg2:函数需要的参数
利用构造函数和call
继承父类的属性
核心原理:在子类的构造函数内部调用父类型的构造函数,并通过call()把父类型的this指向改成指向子类型的this,这样在父类构造函数的运行过程中就可以对子类进行赋值,从而可以实现子类型继承父类型的属性。
// 借用父构造函数继承属性
// 1. 父构造函数
function Father(uname, age) {
// this 指向父构造函数的对象实例
this.uname = uname;
this.age = age;
}
// 2 .子构造函数
function Son(uname, age, score) {
// this 指向子构造函数的对象实例
Father.call(this, uname, age);
this.score = score;
}
var son = new Son('刘德华', 18, 100);
console.log(son); // 包含 uname、age、score三个属性
利用 原型对象
继承父类的方法
一般情况下,对象的方法都在构造函数的原型对象prototype中设置,通过构造函数无法继承父类方法。
核心原理:
- 在定义子类共享方法之前,先让子类的prototype原型对象 = new 父类()
- 父类实例化后会另外开辟空间,再让这块空间赋值给子类原型对象,这样后续在修改子类原型对象时不会影响到父类原型对象
Son.prototype = Father.prototype;
为什么要做第二步?因为这样直接赋值会有问题,如果修改了子原型对象,父原型对象也会跟着一起变化- 进行完赋值操作后,因为子类原型对象本身是包含子类构造函数的,在赋值后就被覆盖了,因此需要我们手动添加子类构造函数到子类原型对象中
// 1. 父构造函数
function Father(uname, age) {
// this 指向父构造函数的对象实例
this.uname = uname;
this.age = age;
}
Father.prototype.money = function () {
console.log(100000);
};
// 2 .子构造函数
function Son(uname, age, score) {
this.score = score;
}
// Son.prototype = Father.prototype; 这样直接赋值会有问题,如果修改了子原型对象,父原型对象也会跟着一起变化
Son.prototype = new Father();
// 如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数
Son.prototype.constructor = Son;
// 这个是子类专门的方法,此时对子类原型对象进行操作,不会影响到父类原型对象
Son.prototype.exam = function () {
console.log('孩子要考试');
}
var son = new Son('刘德华', 18, 100);
此时,console.log(son)的结果是:
即包含子类的属性score、对象原型__proto__
,且对象原型中包括子类构造函数和父类exam方法。
类的本质
ES6中新增的class类的创建方式,其实本质就是上述的构造函数创建类的方法。
- class本质还是function.
- 类的所有方法都定义在类的prototype属性上
- 类创建的实例,里面也有__proto__ 指向类的prototype原型对象
- 所以ES6的类它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
- 所以ES6的类其实就是语法糖.
- 语法糖:语法糖就是一种便捷写法. 简单理解, 有两种方法可以实现同样的功能, 但是一种写法更加清晰、方便,那么这个方法就是语法糖
ES5中的新增方法
数组方法
forEach() 迭代
该方法可以对数组进行迭代操作,在回调函数中对数组的每个元素进行处理,类似于强化版的for of。
array.forEach(function (currentValue, index, arr), [thisArg])
参数:
- currentValue:当前 元素的值
- index:当前元素的索引
- arr:数组对象本身
- forEach第二个参数是thisArg,可选参数,指定当执行回调函数时,内部的this值
filter() 筛选
filter()方法会返回一个新的数组,旧数组中符合函数指定条件的元素才会加入新数组,用于筛选数组元素。
array.filter(function (currentValue, index, arr))
- 该方法需要用一个变量去接受其返回的新数组
- currentValue:数组当前元素的值
- index:数组当前元素的索引号
- arr:数组对象本身
some() 检测
some()方法用于检测数组中是否有元素满足指定的条件,满足则返回true,反之为false。
array.some(function(currentValue, index, arr))
- 返回值是布尔值,如果查找到符合条件的元素就返回true,如果查不到就返回false
- currentValue:数组当前元素的值
- index:数组当前元素的索引号
- arr:数组对象本身
- 如果找到了一个满足条件的元素,就会终止循环,不再查找
字符串方法
trim() 去除空格
trim()方法可以将一个字符串的两端的空格全部删除。
str.trim()
该方法不会影响自身,返回一个新的去除空格后的字符串。
对象方法
Object.keys(obj)
该方法用于获取对象自身的所有属性的名字。
Object.keys(obj)
- 效果类似for in
- 返回一个由属性名字符串构成的数组
Object.defineProperty 定义、修改和配置对象的属性
该方法可以对对象的属性进行新增、修改值、修改权限参数等。
Object.defineProperty(obj, prop, descriptor)
- obj:目标对象
- prop:想要定义或修改的属性名
- descriptor:类型为对象,目标属性所拥有的特性,共可以设置四个特性
- value:属性的值,默认为undefined
- writable:设置该属性的值是否可以重写,传true | false,默认为false
- enumerable:设置该属性是否可以被枚举(用
Object.keys(obj)
来获取属性名),传true | false,默认为false - configurable:设置该属性是否可以被删除或被修改特性,传true | false,默认为false
- getter:设置该属性被读取时的方法
- setter:设置该属性被赋值时的方法
需要注意的是,上述特性中的默认值,当该对象不含这个属性名,你使用该方法新建这个属性名时,默认设定的特性值。(即如果使用别的方法创建的对象属性,是不会不可重写、不可枚举和不可删除的,其他方法如 在构造函数中赋值、直接定义赋值)。
descriptor
分为两类:数据描述符
和存取描述符
,其中,数据描述符包括(configurable、enumerable、writable、value),存取描述符包括(configurable、enumerable、getter、setter)。
Object.defineProperty(obj, 'address', {
value: '中国山东蓝翔技校xx单元',
// 如果值为false 不允许修改这个属性值 默认值也是false
writable: true,
// enumerable 如果值为false 则不允许遍历, 默认的值是 false
enumerable: true,
// configurable 如果为false 则不允许删除这个属性 默认为false
configurable: true
});
函数进阶
函数的定义和调用
定义方式
函数的定义方式共有三种:
- 函数声明方式:
function 函数名() {}
命名函数 - 函数表达式:
var fn = function() {}
匿名函数 - 构造函数方式:
var fn = new Function('参数1', '参数2', ..., '函数体')
注意点:
- 第三种方式里,参数必须都是字符串格式
- 第三种方式的执行效率低,不方便书写,因此较少使用
- 所有函数都是Function的实例(对象),Function也是一个类,构造函数
- 函数也属于对象,函数实例
fn.__proto__
指向Function原型对象(Function.prototype)
函数的调用方式
目前我们一共接触到了六种函数,分别是
- 普通函数
- 对象的方法
- 构造函数
- 绑定事件函数
- 定时器函数
- 立即执行函数
调用方式如下:
// 1. 普通函数
function fn() {
console.log('人生的巅峰');
}
fn();
fn.call()
// 2. 对象的方法
var o = {
sayHi: function() {
console.log('人生的巅峰');
}
}
o.sayHi();
// 3. 构造函数
function Star() {};
var ldh = new Star();
// 4. 绑定事件函数
btn.onclick = function() {}; // 点击了按钮就可以调用这个函数
// 5. 定时器函数
setInterval(function() {}, 1000); 这个函数是定时器自动1秒钟调用一次
// 6. 立即执行函数
(function() {
console.log('人生的巅峰');
})();
// 立即执行函数是自动调用
函数中this的指向
this的指向,是当我们调用函数的时候确定的,调用方式的不同决定了this的指向不同,一般this都指向函数的调用者。
改变函数内部this指向
JavaScript为我们提供了一些函数方法来更优雅的处理函数内部this的指向问题,常用的有bind()、call()、apply()
。
call() 方法
call()方法调用一个对象。简单理解就是调用函数的方式,但是它可以改变函数的this指向。
function.call(thisArg, arg1, arg2, ...)
注意:
- thisArg:指定的函数this指向的对象
- arg1,arg2:函数接受的传递参数
- 返回值就是函数的返回值
- 这个方法一般用于子类继承父类,详见上文
apply() 方法
apply()方法调用一个对象。简单理解就是调用函数的方式,但是它可以改变函数的this指向。
注意:call()方法的作用和 apply() 方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。
function.apply(thisArg, [argsArray])
注意:
- thisArg:指定的函数运行时的this值
- argsArray:一个数组或类数组对象,其中的数组元素将作为单独的参数传给function函数
- 最终,函数function接收到的参数个数取决于argsArray.length,但这要求function能接受可变长度的参数,即
function fn(...args){}
- 返回值就是函数的返回值
- 这个方法一般用于数组的处理,比如使用 Math.max()求数组的最大值
var arr = [1,2,3,4,5];
Math.max.apply(Math, arr); // return 为5
bind() 方法
bind()方法不会调用函数,但是可以改变函数内部的 this指向。
function.bind(thisArg, arg1, arg2, ...)
- thisArg:指定的函数this指向的对象
- arg1,arg2:函数接受的传递参数
- 返回值是一个原函数的拷贝函数,但是其this指向已被指定,参数也被传递了
- 如果我们只想改变函数this的指向,并且不想在当时就立刻调用这个函数,可以使用bind
三种方法总结
相同点:
都可以改变函数内部的this指向
不同点:
- call 和 apply 会调用函数, 并且改变函数内部this指向.
- call 和 apply 传递的参数不一样, call 传递参数 aru1, aru2…形式 apply 必须数组形式[arg]
- bind 不会调用函数, 可以改变函数内部this指向.
主要应用场景:
- call 经常做继承
- apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值
- bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向
严格模式 strict
什么是严格模式
JavaScript 除了提供正常模式外,还提供了严格模式(strict mode)。
ES5 的严格模式是采用具有限制性 JavaScript 变体的一种方式,即在严格的条件下运行 JS 代码。
严格模式在 IE10 以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略。
严格模式对正常的 JavaScript 语义做了一些更改:
- 消除了 Javascript 语法的一些不合理、不严谨之处,减少了一些怪异行为。
- 消除代码运行的一些不安全之处,保证代码运行的安全。
- 提高编译器效率,增加运行速度。
- 禁用了在 ECMAScript 的未来版本中可能会定义的一些语法,为未来新版本的 Javascript 做好铺垫。比如一些保留字如:class, enum, export, extends, import, super 不能做变量名
开启严格模式
严格模式可以应用到整个脚本或个别函数中。
因此在使用时,我们可以将严格模式分为为脚本开启严格模式和为函数开启严格模式两种情况。
① 为脚本开启严格模式
需要在所有语句之前写一行语句:'use strict'
,因为这句话加了引号,所以不兼容的老版本浏览器会把它当作普通字符串忽略,不会报错
<script>
'use strict'
</script>
改进:有的脚本是严格模式,有的是正常模式,这样不利于文件合并,所以可以将整个脚本文件的代码放在一个立即执行的匿名函数中,独立创建一个作用域而不影响其他脚本文件。
<script>
(function (){
"use strict";
var num = 10;
function fn() {}
})();
</script>
② 为函数开启严格模式
要给某个函数开启严格模式,需要把“use strict”; (或 'use strict'; )
声明放在函数体所有语句之前。
function fn(){
"use strict";
return "这是严格模式。";
}
严格模式下的变化
严格模式对JavaScript的部分语法和行为做出了一些改变和限制。
① 变量
- 在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,变量都必须先用var 命令声明,然后再使用。a未声明的情况下,
a = 1
不可使用 - 严禁删除已经声明变量。例如,delete x; 语法是错误的。
var a = 1; delete a;
错误
② this指向
- 以前在全局作用域函数中的 this 指向 window 对象,严格模式下全局作用域中函数中的 this 是 undefined。
- 以前构造函数时不加 new也可以调用,当普通函数,this 指向全局对象,严格模式下,如果构造函数不加new调用, this 指向的是undefined 如果给他赋值则会报错
- 不变:new 实例化的构造函数指向创建的对象实例。
- 不变:定时器 this 还是指向 window,事件、对象还是指向其调用者。
严格模式下全局作用域中函数中的 this 是 undefined。
function fn() {
console.log(this); // undefined。
}
fn();
// 严格模式下,如果 构造函数不加new调用, this 指向的是undefined 如果给他赋值则 会报错.
function Star() {
this.sex = '男';
}
Star(); // 此时会报错,因为this指向了undefined,而变量sex不可以赋值给undefined
③ 函数
- 函数不能有重名的参数。
function fn (a, a, b) {...}
这种就不可 - 函数必须声明在顶层。新版本的 JavaScript 会引入“块级作用域”( ES6 中已引入)。为了与新版本接轨,不允许在非函数的代码块内声明函数。 如在for循环内部、if判断语句内部等是不可声明函数的。
更多严格模式要求参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
高阶函数
高阶函数就是对其他函数进行操作的函数,它接收函数作为参数 或 将函数作为返回值输出。
function fn (callback) {
callback && callback(); // 如果有传回调函数,就执行
}
// 此时,fn就是一个高阶函数,因为它接收函数作为参数
fn(function () { alert('hi') })
function fn () {
return function () {}
}
// 此时,fn就是一个高阶函数,因为它的返回值是一个函数
fn();
函数也是一种数据类型,同样可以作为参数传递给另一个函数使用,最典型的就是作为回调函数。
闭包
推荐阅读:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
变量作用域
变量根据作用域的不同分为两种:全局变量和局部变量。
- 函数内部可以使用全局变量。
- 函数外部不可以使用局部变量。
- 当函数执行完毕,本作用域内的局部变量会销毁。
词法作用域
一篇把词法作用域讲的很好的文章:https://zhuanlan.zhihu.com/p/125568209
作用域就是一种编程语言存取变量的规则。
在编程语言中,作用域一般有两种工作模式。一种是应用最为普遍的词法作用域,另外一种叫做动态作用域。而JavaScript中的作用域就是词法作用域。
什么叫词法作用域?顾名思义,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪来决定的。即根据你写的代码的语句来确定作用域。
var a = 1;
function bar(){
console.log(a);
}
function foo(){
var a = 2;
bar();
}
foo();
上述这段代码的运行结果是1还是2?
从代码可知,bar函数是在foo函数的内部被调用的,但是bar函数是在最外层被定义的,因此其作用域是最外层的全局作用域,而bar和foo函数之间的作用域没有嵌套关系,所以在执行foo函数内部的bar函数时,其a
是去全局作用域里找,即a = 1
。
因此运行结果是1。
总结:
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定,这就是JavaScript的词法作用域。
除非你使用with或者eval欺骗它。
闭包的定义
闭包(closure)就是指有权访问另一个函数作用域中的变量的函数,闭包是由函数以及声明该函数的词法环境组合而成的,该环境包含了这个闭包创建时作用域内的任何局部变量。
简单理解就是:一个作用域可以访问另外一个函数作用域的局部变量。
一个最简单的闭包例子:
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
在本例子中,myFunc
是执行 makeFunc
时创建的 displayName
函数实例的引用。displayName
的实例维持了一个对它的词法环境(变量 name
存在于其中)的引用。因此,当 myFunc
被调用时,变量 name
仍然可用,其值 Mozilla
就被传递到alert
中。
说人话就是:这里的闭包就是displayName
函数,通过它可以访问到makeFunc
函数作用域内的局部变量name
,而makeFunc
函数又被赋值给了myFunc
变量,因此我们可以在外部(非makeFunc
作用域下)访问到makeFunc
函数中的属性(name
是私有属性),这样就实现了私有属性的访问,且不暴露私有属性。
闭包的特性
- 函数嵌套函数;
- 内部函数使用外部函数的参数和变量;
- 参数和变量不会被垃圾回收机制回收。
闭包的优缺点
闭包的优点:
- 可以将一个变量长期保存在内存中;
- 避免全局变量被污染;
- 私有成员的存在。
闭包的缺点:
- 常驻内存,增加内存使用量;
- 使用不当造成内存泄漏。
应用场景一(经典)
该应用体现了闭包:可以将一个变量长期保存在内存中,避免全局变量被污染,同时增加了内存的使用量
案例描述:
当前页面有一个列表,我们想要实现点击每个小li,都可以打印其索引号
<ul class="nav">
<li>榴莲</li>
<li>臭豆腐</li>
<li>鲱鱼罐头</li>
<li>大猪蹄子</li>
</ul>
想当然却错误的做法:
var lis = document.querySelector('.nav').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function() {
console.log(i);
}
}
上述代码最终的打印结果:不论点击那个li,都打印4
因为在我们为每个li绑定点击事件时,回调函数将不会立即执行,而是放到任务队列
中等待执行,而在触发点击事件后执行回调函数时,当前的for循环已执行完成,此时i
的值等于4,然后i被传递给回调函数,所以回调函数的值就只能是4了。
利用闭包实现的做法:
// 闭包应用-点击li输出当前li的索引号
// 1. 我们可以利用动态添加属性的方式
var lis = document.querySelector('.nav').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
lis[i].index = i;
lis[i].onclick = function() {
// console.log(i);
console.log(this.index);
}
}
// 2. 利用闭包的方式得到当前小li 的索引号
for (var i = 0; i < lis.length; i++) {
// 利用for循环创建了4个立即执行函数
// 立即执行函数也成为小闭包因为立即执行函数里面的任何一个函数都可以使用它的i这变量
(function(i) {
lis[i].onclick = function() {
console.log(i);
}
})(i);
}
该方法中,这个点击事件的回调函数就是闭包,其可以使用匿名立即执行函数的参数i
,在for循环绑定点击事件时,四个匿名执行函数都创建了其独立的作用域(词法环境),作用域内有一个变量i
,这个i
一直保存在内存中,当闭包(回调函数)执行时,就可以从内存中取到立即执行函数在执行时收到的参数i
,其值分别是0,1,2,3。
应用场景二
对四个元素都设置一个定时器,在三秒之后分别打印该元素的内容
<ul class="nav">
<li>榴莲</li>
<li>臭豆腐</li>
<li>鲱鱼罐头</li>
<li>大猪蹄子</li>
</ul>
<script>
// 闭包应用-3秒钟之后,打印所有li元素的内容
var lis = document.querySelector('.nav').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
(function(i) {
setTimeout(function() {
console.log(lis[i].innerHTML);
}, 3000)
})(i);
}
</script>
实现思想与上一案例类似。
应用场景三(利用闭包模拟实现私有方法)
引自MDN-闭包
有部分编程语言,比如Java,是支持将方法声明为私有的,即私有方法只能被同一个类中的其他方法调用,而不可被直接使用。
而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。
私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern):
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
在上述示例中,存在三个闭包:Counter.increment
,Counter.decrement
和 Counter.value
,它们都共享一个词法环境。
那是什么样的词法环境呢?该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问,也就是说,它们都可以被称为私有成员。
这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter
变量和 changeBy
函数。
上述是私有方法模拟的简单实现,下面将介绍一个充分运用闭包特性的案例:
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
我们把上文代码中的匿名函数改为存放给一个变量,并利用这个变量来创建多个计数器。
请注意两个计数器Counter1
和 Counter2
是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter
。
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。
以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。
一道关于this指向和闭包的思考题
Q:求console.log(object.getNameFunc()())的输出值
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
return function() {
return this.name;
};
}
};
console.log(object.getNameFunc()())
------------该语句就等于以下代码------------
var f = object.getNameFunc();
// 类似于
var f = function() {
return this.name;
}
f();
首先执行object.getNameFunc()
,并返回一个匿名函数,再执行f()
,此时就相当于这个f就是一个函数,对其进行了调用,又因为这个f是全局作用域,因此f()
== window.f()
,就是window调用了这个函数,则函数内的this指向window,window.name = 'The Window
,最终结果是The Window
。
递归
递归的概念
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。
简单理解:函数内部自己调用自己,这个函数就是递归函数
递归函数的作用和循环效果类似
由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return。
示例一:求n的阶乘
实现一个乘法器,接受一个数字n,求1 * 2 * ……* n 的值,即n!
function fn (n) {
if (n == 1):
return 1;
return n * fn(n - 1);
}
示例二:斐波那契数列
斐波那契数列:当前数的值是前两个数之和
1、1、2、3、5、8、13、21…
function fn (n) {
if (n === 1 || n === 2)
return 1;
return fn(n - 1) + fn(n - 2);
}
浅拷贝、深拷贝
两者区别:
- 浅拷贝只是拷贝一层, 更深层次对象级别的只拷贝引用,即如果对象内还有一个属性
a
是对象,那么对这个对象a
进行修改,原来的拷贝和被拷贝对象里的a
属性的值都会被同步修改; - 深拷贝拷贝多层, 每一级别的数据都会拷贝,而且不会被互相影响。
extend 浅、深拷贝
在jQuery中,我们学习了如何使用extend
对对象进行深拷贝,回顾如下:
如果想把某个对象拷贝或合并给另一个对象使用,可采用$.extend()
方法。
语法:$.extend([deep], target, object1, [objectN])
- deep:true是就是深拷贝,默认为false浅拷贝
- target:要得到的目标对象
- object1:待拷贝到target的第一个对象
- objectN:待拷贝到target的第N个对象
- 浅拷贝:把被拷贝的对象中复杂数据类型的值的地址拷贝给目标对象,此时如果修改目标对象的该值,会影响到旧的被拷贝对象
- 深拷贝把里面的数据完全复制一份给目标对象 如果里面有不冲突的属性,会合并到一起
var targetObj = {
id: 0,
msg: {
sex: '男'
}
};
var obj = {
id: 1,
name: "andy",
msg: {
age: 18
}
};
$.extend(targetObj, obj);
console.log(targetObj); // 会覆盖targetObj 里面原来的数据
assign 浅拷贝
这个方法是es6新增的浅拷贝方法。
Object.assign(target, ...sources)
- target:最终返回的目标对象
- sources:被拷贝的源对象
示例:
var obj = {
id: 1,
name: 'andy',
msg: {
age: 18
}
};
var o = {};
Object.assign(o, obj);
console.log(o) // 内部的内容就和obj一致
深拷贝
除了使用Object.extent()方法外,还可以利用递归函数来实现深拷贝。
// 深拷贝拷贝多层, 每一级别的数据都会拷贝.
var obj = {
id: 1,
name: 'andy',
msg: {
age: 18
},
color: ['pink', 'red']
};
var o = {};
// 封装函数
function deepCopy(newobj, oldobj) {
for (var k in oldobj) {
// 判断我们的属性值属于那种数据类型
// 1. 获取属性值 oldobj[k]
var item = oldobj[k];
// 2. 判断这个值是否是数组
if (item instanceof Array) {
newobj[k] = [];
deepCopy(newobj[k], item)
} else if (item instanceof Object) {
// 3. 判断这个值是否是对象
newobj[k] = {};
deepCopy(newobj[k], item)
} else {
// 4. 属于简单数据类型
newobj[k] = item;
}
}
}
deepCopy(o, obj);
正则表达式
查阅手册:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions
定义
**正则表达式( Regular Expression )**是用于匹配字符串中字符组合的模式。
在 JavaScript中,正则表达式也是对象。
正则表通常被用来检索、替换那些符合某个模式(规则)的文本,例如验证表单:用户名表单只能输入英文字母、数字或者下划线, 昵称输入框中可以输入中文(匹配)。此外,正则表达式还常用于过滤掉页面内容中的一些敏感词(替换),或从字符串中获取我们想要的特定部分(提取)等 。
特点
- 灵活性、逻辑性和功能性非常的强。
- 可以迅速地用极简单的方式达到字符串的复杂控制。
- 对于刚接触的人来说,比较晦涩难懂。比如: ^\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)*$
- 实际开发,一般都是直接复制写好的正则表达式. 但是要求会使用正则表达式并且根据实际情况修改正则表达式. 比如用户名:
/^[a-z0-9_-]{3,16}$/
基本使用方法
创建正则表达式
在JavaScript中,我们可以通过两种方式创建一个正则表达式。
- 通过调用RegExp对象的构造函数来创建
var re = new RegExp(/表达式/);
- 通过字面量创建
var re = /表达式/;
test() 测试正则表达式是否符合
该方法用于检测字符串是否符合你所定义的郭泽,如果符合则返回true,反之为false。
regExpObj.test(str)
- regexObj 是写的正则表达式
- str 我们要测试的文本
- 检测str文本是否符合我们写的正则表达式规范
特殊符号、语法规则
边界符
/^abc/
表示必须以abc开头
/abc$/
表示必须以abc结尾
/^abc$/
表示必须整个字符串就是abc
字符类
/[abc]/
表示字符串包含a或b或c即可
/[a-z]/
表示字符串包含a到z的任一小写字母即可
/^[abc]/
表示字符串的开头必须是a或b或c
/^[abc]$/
表示字符串必须是一个字符,且是a或b或c
/^[a-zA-Z0-9_-]$/
表示字符串必须是一个字符,且可以是a到z的大写或小写字母、或是数字0到9、或是下划线、或是短横线
/^[^a-z]$/
注意,在字符集合(方括号)中,^
符合表示取反,即字符串必须是一个字符,且不得是a到z的小写字母
量词符
量词符用来设定某个模式出现的次数。
注意:{n,m}逗号后面不能加空格
预定义类
预定义类是指某些常见模式的简写方式。
switch修饰符
/regExp/[switch]
switch修饰符是设定这个正则表达式按照什么模式来匹配:
- g:全局匹配,即可匹配一段话内多个符合条件的字串
- i:忽略大小写
- gi:全局匹配 + 忽略大小写
替换
replace()
方法可以实现替换字符串操作,用来替换的参数可以是一个字符串或是一个正则表达式。
str.replace(regexp/substr, replacement)
- regexp/substr:被替换的字符串,可以是子字符串或正则表达式
- replacement:要替换成的字符串(用这个字符串来替换前者)
- 返回值:返回一个替换完毕的新字符串
var a = '我是我'
var b = a.replace(/我/g, '你')
console.log(b) // ‘你是你’