JavaScript 面向对象
1. 面向对象编程介绍
1.1 两大编程思想
-
面向过程,是分析出解决问题所需要的步骤,然后用函数把这些步骤一步步实现,使用的时候再一个一个的依次调用。面向过程,是按照步骤解决问题。
优点:性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机
缺点:没有面向对象易维护、易复用、易扩展
-
面向对象,是把事务分解成为一个个对象,然后由对象之间分工与合作。面向对象是以对象功能来划分问题,而不是步骤。
优点:易维护、易复用、易扩展,由于封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点:性能比面向过程低
2. ES6 中的类和对象
2.1 对象
在 JavaScript 中,对象是一组无序的相关属性和方法的集合,所有的事物都是对象,例如字符串、数值、数组、函数等。
对象是由属性和方法组成的:
-
属性:事物的特征,在对象中用属性来表示(常用名词)
-
方法:事物的行为,在对象中用方法来表示(常用动词)
2.2 类 class
在 ES6 中新增加了类的概念,可使用 class 关键字声明一个类,以这个类来实例化对象。
-
类抽象了对象的公共部分,它泛指某一大类(class)
-
对象特指某一个,通过类实例化一个具体的对象
2.3 创建类
// 语法
class name {
// class body
}
//创建实例
var xx = new name();
注意:类必须使用 new 实例化对象
2.4 类 constructor 构造函数
constructor() 方法是类的构造函数(默认方法),用于传递参数,返回实例对象,通过 new 命令生成对象实例时,自动调用该方法。如果没有显示定义, 类内部会自动给我们创建一个constructor()
// 语法
class Person {
constructor(name,age) { // constructor 构造方法或者构造函数
this.name = name;
this.age = age;
}
}
// 创建实例
var ldh = new Person('刘德华', 18);
console.log(ldh.name)
2.5 类添加方法
//语法
class Person {
constructor(name,age) { // constructor 构造器或者构造函数
this.name = name;
this.age = age;
}
say() {
console.log(this.name + '你好');
}
}
// 创建实例
var ldh = new Person('刘德华', 18);
ldh.say()
注意: 方法之间不能加逗号分隔,同时方法不需要添加 function 关键字。
3. 类的继承
3.1 继承
子类可以继承父类的一些属性和方法。
//语法
class Father{ // 父类
}
class Son extends Father { // 子类继承父类
}
// 案例
class Father {
constructor(surname) {
this.surname= surname;
}
say() {
console.log('你的姓是' + this.surname);
}
}
class Son extends Father{ // 这样子类就继承了父类的属性和方法
}
var damao= new Son('刘');
damao.say();
3.2 super关键字
super 关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数
- 调用父类的构造函数
// 语法
class Person { // 父类
constructor(surname){
this.surname = surname;
}
}
class Student extends Person { // 子类继承父类
constructor(surname,firstname){
super(surname); // 调用父类的constructor(surname)
this.firstname = firstname; // 定义子类独有的属性
}
}
// 案例
class Father {
constructor(surname) {
this.surname = surname;
}
saySurname() {
console.log('我的姓是' + this.surname);
}
}
class Son extends Father { // 这样子类就继承了父类的属性和方法
constructor(surname, fristname) {
super(surname); // 调用父类的构造函数 constructor(surname)
this.fristname = fristname;
}
sayFristname() {
console.log("我的名字是:" + this.fristname);
}
}
var damao = new Son('刘', "德华");
damao.saySurname();
damao.sayFristname();
注意: 子类在构造函数中使用super, 必须放到 this 前面 (必须先调用父类的构造方法,在使用子类构造方法)
- 调用父类的普通函数
class Father {
say() {
return '我是爸爸';
}
}
class Son extends Father { // 这样子类就继承了父类的属性和方法
say() {
// super.say() super 调用父类的方法
return super.say() + '的儿子';
}
}
var damao = new Son();
console.log(damao.say());
3.3 三个注意点
- 在 ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象
- 类里面的共有属性和方法一定要加this使用
- 类里面的this指向问题:constructor 里面的this指向实例对象, 方法里面的this 指向这个方法的调用者
构造函数和原型
1.构造函数和原型
1.1 概述
在典型的 OOP 的语言中(如 Java),都存在类的概念,类就是对象的模板,对象就是类的实例,但在 ES6之前, JS 中并没用引入类的概念。在 ES6之前 ,对象不是基于类创建的,而是用一种称为构建函数的特殊函数来定义对象和它们的特征。
创建对象可以通过以下三种方式:
- 对象字面量
- new Object()
- 自定义构造函数
1.2 构造函数
构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。
在 JS 中,使用构造函数时要注意以下两点:
- 构造函数用于创建某一类对象,其首字母要大写
- 构造函数要和 new 一起使用才有意义
new在执行时会做四件事情:
①在内存中创建一个新的空对象。
②让 this 指向这个新的对象。
③执行构造函数里面的代码,给这个新对象添加属性和方法。
④返回这个新对象(所以构造函数里面不需要 return )。
JavaScript 的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的 this 上添加。通过这两种方式添加的成员,就分别称为静态成员和实例成员。
-
静态成员:在构造函数本上添加的成员称为静态成员,只能由构造函数本身来访问
-
实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问
1.3 构造函数的问题
构造函数方法很好用,但是存在浪费内存的问题。
function Star(uname, age) {
this.uname = uname;
this.age = age;
this.sing = function() {
console.log('我会唱歌');
}
}
var ldh = new Star('刘德华', 18);
var zxy = new Star('张学友', 19);
1.4 构造函数原型 prototype
构造函数通过原型分配的函数是所有对象所共享的。
JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。注意这个prototype 就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
- 原型是一个对象,我们也称为 prototype 为原型对象
- 原型的作用是共享方法
1.5 对象原型 __ proto __
对象都会有一个属性 __ proto __ 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 __ proto __ 原型的存在。
- __ proto __对象原型和原型对象 prototype 是等价的
- __ proto __对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype
1.6 constructor 构造函数
对象原型( __ proto __)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。
constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
// 很多情况下,我们需要手动的利用constructor 这个属性指回 原来的构造函数
// 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('我会演电影');
}
}
var ldh = new Star('刘德华', 18);
var zxy = new Star('张学友', 19);
console.log(Star.prototype);
console.log(ldh.__proto__);
console.log(Star.prototype.constructor);
console.log(ldh.__proto__.constructor);
1.7 构造函数、实例、原型对象三者之间的关系
1.8 原型链
1.9 对象成员查找规则
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
- 如果没有就查找它的原型(也就是 __ proto __指向的 prototype 原型对象)。
- 如果还没有就查找原型对象的原型(Object的原型对象)。
- 依此类推一直找到 Object 为止(null)。
- __ proto __对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。
1.10 原型对象this指向
构造函数中的this 指向实例对象.
原型对象里面放的是方法, 这个方法里面的this 指向的是 这个方法的调用者, 也就是这个实例对象.
1.11 扩展内置对象
可以通过原型对象,对原来的内置对象进行扩展自定义的方法。比如给数组增加自定义求偶数和的功能。
// 原型对象的应用 扩展内置对象方法
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());
console.log(Array.prototype);
var arr1 = new Array(11, 22, 33);
console.log(arr1.sum());
**注意:**数组和字符串内置对象不能给原型对象覆盖操作 Array.prototype = {} ,只能是 Array.prototype.xxx = function(){} 的方式。
2. 继承
ES6之前并没有给我们提供 extends 继承。我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承。
2.1 call()
调用这个函数, 并且修改函数运行时的 this 指向
fun.call(thisArg, arg1, arg2, ...)
- thisArg :当前调用函数 this 的指向对象
- arg1,arg2:传递的其他参数
2.2 借用构造函数继承父类型属性
核心原理: 通过 call() 把父类型的 this 指向子类型的 this ,这样就可以实现子类型继承父类型的属性。
// 父类
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 子类
function Student(name, age, sex, score) {
Person.call(this, name, age, sex); // 此时父类的 this 指向子类的 this,同时调用这个函数
this.score = score;
}
var s1 = new Student('zs', 18, '男', 100);
console.dir(s1);
2.3 借用原型对象继承父类型方法
一般情况下,对象的方法都在构造函数的原型对象中设置,通过构造函数无法继承父类方法。
核心原理:
- 将子类所共享的方法提取出来,让子类的 prototype 原型对象 = new 父类()
- 将子类的 constructor 重新指向子类的构造函数
// 借用父构造函数继承属性
// 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 指向子构造函数的对象实例
Father.call(this, uname, age);
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);
console.log(Father.prototype);
console.log(Son.prototype.constructor);
本质:子类原型对象等于是实例化父类,因为父类实例化之后另外开辟空间,就不会影响原来父类原型对象
3. 类的本质
- class本质上还是function。
- 类的所有方法都定义在类的prototype属性上。
- 类创建的实例,里面也有__ proto __ 指向类的prototype原型对象。
- ES6的类的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
- ES6的类其实就是语法糖。
- 语法糖:语法糖就是一种便捷写法。简单理解, 有两种方法可以实现同样的功能,但是一种写法更加清晰、方便,那么这个方法就是语法糖。
4. ES5中的新增方法
ES5 中给我们新增了一些方法,可以很方便的操作数组或者字符串,这些方法主要包括:
-
数组方法
-
字符串方法
-
对象方法
4.1 数组方法
迭代(遍历)方法:forEach()、map()、filter()、some()、every()
forEach()
array.forEach(function(currentValue, index, arr))
-
currentValue:数组当前项的值
-
index:数组当前项的索引
-
arr:数组对象本身
map()
var numbers1 = [45, 4, 9, 16, 25];
var numbers2 = numbers1.map(myFunction);
function myFunction(value, index, array) {
return value * 2;
}
- map() 方法通过对每个数组元素执行函数来创建新数组。
- map()方法不会对没有值的数组元素执行函数。
- map()方法不会更改原始数组。
filter()
array.filter(function(currentValue, index, arr))
-
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素,主要用于筛选数组
-
它直接返回一个新数组
-
currentValue: 数组当前项的值
-
index:数组当前项的索引
-
arr:数组对象本身
some()
array.some(function(currentValue, index, arr))
- some() 方法用于检测数组中的元素是否满足指定条件。(查找数组中是否有满足条件的元素)
- 它的返回值是布尔值,如果查找到这个元素,就返回true,如果查找不到就返回false。
- 如果找到第一个满足条件的元素,则终止循环,不再继续查找.
- currentValue: 数组当前项的值
- index:数组当前项的索引
- arr:数组对象本身
every()
var numbers = [45, 4, 9, 16, 25];
var allOver18 = numbers.every(myFunction);
function myFunction(value, index, array) {
return value > 18;
}
every()
方法检查所有数组值是否通过测试。(数组中是否每一项都符合要求)- 返回true或者false
4.2 字符串方法
trim() 方法会从一个字符串的两端删除空白字符。
str.trim()
trim() 方法并不影响原字符串本身,它返回的是一个新的字符串。
4.3 对象方法
- Object.keys() 用于获取对象自身所有的属性
Object.keys(obj)
-
效果类似 for…in
-
返回一个由属性名组成的数组
- Object.defineProperty() 定义对象中新属性或修改原有的属性。(了解)
Object.defineProperty(obj, prop, descriptor)
- obj:必需。目标对象
- prop:必需。需定义或修改的属性的名字
- descriptor:必需。目标属性所拥有的特性
Object.defineProperty() 第三个参数 descriptor 说明: 以对象形式 { } 书写
- value: 设置属性的值 默认为undefined
- writable: 值是否可以重写。true | false 默认为false
- enumerable: 目标属性是否可以被枚举。true | false 默认为 false
- configurable: 目标属性是否可以被删除或是否可以再次修改特性 true | false 默认为false
函数
1. 函数的定义和调用
1.1 函数的定义方式
-
自定义函数(命名函数)
function fn() {};
-
函数表达式(匿名函数)
var fun = function() {};
-
利用 new Function(‘参数1’,‘参数2’,‘函数体’) 效率低,不方便书写,较少使用
var f = new Function('a','b','console.log(a+b)'); f(1,2);
所有函数实际上都是 Function 的实例(对象)。
函数也属于对象
1.2 函数的调用方式
-
普通函数
function fn() {}; fn(); fn.call();
-
对象的方法
var o = { sayHi:function(){ } } o.sayHi();
-
构造函数
function Star(){}; new Star();
-
绑定事件函数
btn.onclick = function(){}; //点击按钮即可调用
-
定时器函数
setInterval(function(){},1000); //定时器自动1秒调用一次
-
立即执行函数
(function(){ })(); //立即执行函数是自动调用
2. this
2.1 this的指向
这些 this 的指向,是调用函数时确定的。调用方式的不同决定了this 的指向不同,一般指向调用者。
-
普通函数 this 指向 window
function fn() {}; fn(); fn.call(); window.fn() //完整写法
-
对象的方法 this 指向的是该方法所属对象 o
var o = { sayHi:function(){ } } o.sayHi();
-
构造函数 this 指向 实例对象 ldh
原型对象里面的方法 指向的也是 实例对象 ldh
function Star(){}; Star.prototype.sing = function(){ } var ldh = new Star();
-
绑定事件函数 this 指向 绑定事件对象(函数的调用者) btn
btn.onclick = function(){}; //点击按钮即可调用
-
定时器函数 this 指向的也是 window
setInterval(function(){},1000); //定时器自动1秒调用一次
-
立即执行函数 this 指向的也是 window
(function(){ })(); //立即执行函数是自动调用
2.2 改变函数内部 this 指向
JavaScript 为我们专门提供了一些函数方法来帮我们更优雅的处理函数内部 this 的指向问题,常用的有 bind()、call()、apply() 三种方法。
- call方法及其应用
- 调用函数
- 改变函数内的this指向
- 主要作用是实现继承
// 1. call()
var o = {
name: 'andy'
}
function fn(a, b) {
console.log(this);
console.log(a + b);
};
fn.call(o, 1, 2); // 指向对象o
// call 第一个可以调用函数 第二个可以改变函数内的this指向
// call 的主要作用可以实现继承
function Father(uname, age, sex) {
this.uname = uname;
this.age = age;
this.sex = sex;
}
function Son(uname, age, sex) {
Father.call(this, uname, age, sex);
}
var son = new Son('刘德华', 18, '男');
console.log(son);
- apply方法及其应用
// 2. apply() 应用、运用的意思
var o = {
name: 'andy'
};
function fn(arr) {
console.log(this);
console.log(arr); // 'pink'
};
fn.apply(o, ['pink']); // 指向对象o
// 1. 也是调用函数 第二个可以改变函数内部的this指向
// 2. 但是他的参数必须是数组(伪数组)
// 3. apply 的主要应用 比如说我们可以利用 apply 借助于数学内置对象求数组最大值
// Math.max();
var arr = [1, 66, 3, 99, 4];
var arr1 = ['red', 'pink'];
// var max = Math.max.apply(null, arr);
var max = Math.max.apply(Math, arr);
var min = Math.min.apply(Math, arr);
console.log(max, min);
- bind方法及其应用
<button>点击</button>
<button>点击</button>
<button>点击</button>
<script>
// 3. bind() 绑定、捆绑的意思
var o = {
name: 'andy'
};
function fn(a, b) {
console.log(this);
console.log(a + b);
};
var f = fn.bind(o, 1, 2);
f();
// 1. 不会调用原来的函数 可以改变原来函数内部的this 指向
// 2. 返回的是原函数改变this之后产生的新函数
// 3. 如果有的函数我们不需要立即调用,但是又想改变这个函数内部的this指向此时用bind
// 4. 我们有一个按钮,当我们点击了之后,就禁用这个按钮,3秒钟之后开启这个按钮
// var btn1 = document.querySelector('button');
// btn1.onclick = function() {
// this.disabled = true; // 这个this 指向的是 btn 这个按钮
// // var that = this;
// setTimeout(function() {
// // that.disabled = false; // 定时器函数里面的this 指向的是window
// this.disabled = false; // 此时定时器函数里面的this 指向的是btn
// }.bind(this), 3000); // 这个this 指向的是btn 这个对象
// }
var btns = document.querySelectorAll('button');
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = function() {
this.disabled = true;
setTimeout(function() {
this.disabled = false;
}.bind(this), 2000);
}
}
</script>
- call apply bind 总结
相同点:
都可以改变函数内部的this指向
区别点:
- call 和 apply 会调用函数,bind 则不会调用函数
- call 和 apply 传递的参数不一样,apply参数为数组
主要应用场景
- call 继承
- apply 常与数组有关系,比如借助于数学对象实现数组最大值最小值
- bind 不调用函数 但想改变this指向 比如改变定时器内部的this指向
3. 严格模式
3.1 开启严格模式
严格模式可以应用到整个脚本或个别函数中。因此使用时,我们可以将严格模式分为为脚本开启严格模式和为函数开启严格模式两种情况。
<!-- 为脚本开启严格模式 -->
<script>
'use strict';
// 下面的js代码就会按照严格模式执行代码
</script>
<script>
(function() {
'use strict';
})();
</script>
<!-- 为函数开启严格模式 -->
<script>
// 此时只是给fn函数开启严格模式
function fn() {
'use strict';
// 下面的代码按照严格模式执行
}
function fun() {
// 里面的还是按照普通模式执行
}
</script>
严格模式中的变化
'use strict';
// 1. 我们的变量名必须先声明再使用
// num = 10;
// console.log(num);
var num = 10;
console.log(num);
// 2.我们不能随意删除已经声明好的变量 语法错误
// delete num;
// 3. 严格模式下全局作用域中函数中的 this 是 undefined。
// function fn() {
// console.log(this); // undefined。
// }
// fn();
// 4. 严格模式下,如果构造函数不加new调用, this 指向的是undefined 如果给他赋值则会报错.
// function Star() {
// this.sex = '男';
// }
// // Star();
// var ldh = new Star();
// console.log(ldh.sex);
// 5. 定时器 this 还是指向 window
// setTimeout(function() {
// console.log(this);
// }, 2000);
// a = 1;
// a = 2;
// 6. 严格模式下函数里面的参数不允许有重名
// function fn(a, a) {
// console.log(a + a);
// };
// fn(1, 2);
function fn() {}
// 7.函数必须声明在顶层 允许函数嵌套 但不允许在非函数的代码块内声明函数 如if for等
更多严格模式要求 请参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
4. 高阶函数
高阶函数是对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出
// 高阶函数 函数可以作为参数传递
function fn(a, b, callback) {
console.log(a + b);
callback && callback();
}
fn(1, 2, function() {
console.log('我是最后调用的');
});
$("div").animate({
left: 500
}, function() {
$("div").css("backgroundColor", "purple");
})
函数也是一种数据类型,同样可以作为参数,传递给另外一个函数使用,最典型的就是作为回调函数。同理,函数也可以作为返回值传递回来。
5. 闭包
5.1 变量作用域
变量根据作用域的不同分为两种:全局变量和局部变量。
-
函数内部可以使用全局变量。
-
函数外部不可以使用局部变量。
-
当函数执行完毕,本作用域内的局部变量会销毁。
5.2 什么是闭包
闭包(closure)指有权访问另一个函数作用域中变量的函数 (一个作用域可以访问另外一个函数的局部变量)
// 闭包(closure)指有权访问另一个函数作用域中变量的函数。
// 闭包: 我们fun这个函数作用域 访问了另外一个函数fn里面的局部变量num
function fn() {
var num = 10;
function fun() {
console.log(num);
}
fun();
}
fn();
5.3 闭包的作用
闭包作用:延伸变量的作用范围
// 闭包(closure)指有权访问另一个函数作用域中变量的函数。
// 一个作用域可以访问另外一个函数的局部变量
// 我们fn外面的作用域可以访问fn内部的局部变量
// 闭包的主要作用: 延伸了变量的作用范围 闭包,也是高阶函数的一种用法
function fn() {
var num = 10; // 变量num 所有函数使用完才会销毁
// function fun() {
// console.log(num);
// }
// return fun;
return function() {
console.log(num);
} // return是闭包的主要实现原理
}
var f = fn();
f();
// 类似于
// var f = function() {
// console.log(num);
// }
// var f = function fun() {
// console.log(num);
// }
5.4 闭包的应用
点击 li 输出当前 li 的索引号 (经典面试案例)
// 闭包应用-点击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) {
// console.log(i);
lis[i].onclick = function() {
console.log(i);
// 闭包也不一定是好的 此案例中若一直不点击li,i就不释放,会造成内存泄露
}
})(i);
}
立即执行函数格式
同步任务:for 循环
异步任务:定时器函数,事件函数,ajax,触发了才会执行
5.5 在 chrome 中调试闭包
-
打开浏览器,按 F12 键启动 chrome 调试工具。
-
设置断点。
-
找到 Scope 选项(Scope 作用域的意思)。
-
当我们重新刷新页面,会进入断点调试,Scope 里面会有两个参数(global 全局作用域、local 局部作用域)。
-
当执行到 fn2() 时,Scope 里面会多一个 Closure 参数 ,这就表明产生了闭包。
6. 递归
6.1 什么是递归
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。
// 递归函数 : 函数内部自己调用自己, 这个函数就是递归函数
var num = 1;
function fn() {
console.log('我要打印6句话');
if (num == 6) {
return; // 递归里面必须加退出条件
}
num++;
fn();
}
fn();
-
递归函数的作用和循环效果一样
-
为了防止发生栈溢出(stack overflow)错误,递归里必须加退出条件 return
6.2 浅拷贝和深拷贝
- 浅拷贝
// 浅拷贝只是拷贝一层, 更深层次对象级别的只拷贝引用地址.一变全变
var obj = {
id: 1,
name: 'andy',
msg: {
age: 18
}
};
var o = {};
// for (var k in obj) {
// // k 是属性名 obj[k] 属性值
// o[k] = obj[k];
// }
// console.log(o);
// o.msg.age = 20;
// console.log(obj);
console.log('--------------');
// es6 语法糖
Object.assign(o, obj);
console.log(o);
o.msg.age = 20;
console.log(obj);
- 深拷贝
// 深拷贝拷贝多层, 每一级别的数据都会拷贝.一变不变
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);
console.log(o);
var arr = [];
console.log(arr instanceof Object);
o.msg.age = 20; //一变
console.log(obj); //不变
- 总结
- 浅拷贝只是拷贝一层, 更深层次对象级别的只拷贝引用.
- 深拷贝拷贝多层, 每一级别的数据都会拷贝.
- Object.assign(target, …sources) es6 新增方法可以浅拷贝
正则表达式
正则表达式(Regular Expression)是用于匹配字符串中字符组合的模式。在JavaScript中,正则表达式也是对象。常用于匹配(限制输入)、替换(敏感词)、提取(特定部分)。
// 正则表达式在js中的使用
// 1. 利用 RegExp对象来创建 正则表达式
var regexp = new RegExp(/123/);
console.log(regexp);
// 2. 利用字面量创建 正则表达式
var rg = /123/;
// 3.test 方法用来检测字符串是否符合正则表达式要求的规范
console.log(rg.test(123));
console.log(rg.test('abc'));
1. 特殊字符
1.1 边界符
// 边界符 ^开头 $结尾
var rg = /abc/; // 正则表达式里面不需要加引号 不管是数字型还是字符串型
// /abc/ 只要包含有abc这个字符串返回的都是true
console.log(rg.test('abc'));
console.log(rg.test('abcd'));
console.log(rg.test('aabcd'));
console.log('---------------------------');
var reg = /^abc/; // abc开头
console.log(reg.test('abc')); // true
console.log(reg.test('abcd')); // true
console.log(reg.test('aabcd')); // false
console.log('---------------------------');
var reg1 = /^abc$/; // ^ $ 精确匹配 要求必须是 abc字符串才符合规范
console.log(reg1.test('abc')); // true
console.log(reg1.test('abcd')); // false
console.log(reg1.test('aabcd')); // false
console.log(reg1.test('abcabc')); // false
1.2 字符类
//var rg = /abc/; 只要包含abc就可以
// 字符类: [] 表示有一系列字符可供选择,只要匹配其中一个就可以了
var rg = /[abc]/; // 只要包含有a 或者 包含有b 或者包含有c 都返回为true
console.log(rg.test('andy'));
console.log(rg.test('baby'));
console.log(rg.test('color'));
console.log(rg.test('red'));
var rg1 = /^[abc]$/; // 三选一 只有是a 或者是 b 或者是c 这三个字母才返回 true
console.log(rg1.test('aa'));
console.log(rg1.test('a'));
console.log(rg1.test('b'));
console.log(rg1.test('c'));
console.log(rg1.test('abc'));
console.log('------------------');
var reg = /^[a-z]$/; // 26个英文字母任何一个字母返回 true - 表示的是a 到z 的范围
console.log(reg.test('a'));
console.log(reg.test('z'));
console.log(reg.test(1));
console.log(reg.test('A'));
// 字符组合
var reg1 = /^[a-zA-Z0-9_-]$/; // 26个英文字母(大写和小写都可以)任何一个字母返回 true
console.log(reg1.test('a'));
console.log(reg1.test('B'));
console.log(reg1.test(8));
console.log(reg1.test('-'));
console.log(reg1.test('_'));
console.log(reg1.test('!'));
console.log('----------------');
// 如果中括号里面有^ 表示取反的意思 千万和 我们边界符 ^ 别混淆
var reg2 = /^[^a-zA-Z0-9_-]$/;
console.log(reg2.test('a'));
console.log(reg2.test('B'));
console.log(reg2.test(8));
console.log(reg2.test('-'));
console.log(reg2.test('_'));
console.log(reg2.test('!'));
1.3 量词符
// 量词符: 用来设定某个模式出现的次数
// 简单理解: 就是让下面的a这个字符重复多少次
// var reg = /^a$/;
// * 相当于 >= 0 可以出现0次或者很多次
// var reg = /^a*$/;
// console.log(reg.test('')); // true
// console.log(reg.test('a')); // true
// console.log(reg.test('aaaa')); // true
// + 相当于 >= 1 可以出现1次或者很多次
// var reg = /^a+$/;
// console.log(reg.test('')); // false
// console.log(reg.test('a')); // true
// console.log(reg.test('aaaa')); // true
// ? 相当于 1 || 0
// var reg = /^a?$/;
// console.log(reg.test('')); // 0次为true
// console.log(reg.test('a')); // 1次为true
// console.log(reg.test('aaaa')); // false
// {3} 就是重复3次
// var reg = /^a{3}$/;
// console.log(reg.test('')); // false
// console.log(reg.test('a')); // false
// console.log(reg.test('aaaa')); // false
// console.log(reg.test('aaa')); // true
// {3, } 大于等于3
var reg = /^a{3,}$/;
console.log(reg.test('')); // false
console.log(reg.test('a')); // false
console.log(reg.test('aaaa')); // true
console.log(reg.test('aaa')); // true
// {3,16} 大于等于3 并且 小于等于16 注意中间不要有空格
var reg = /^a{3,6}$/;
console.log(reg.test('')); // false
console.log(reg.test('a')); // false
console.log(reg.test('aaaa')); // true
console.log(reg.test('aaa')); // true
console.log(reg.test('aaaaaaa')); // false
1.4 括号
// 中括号 字符集合.匹配方括号中的任意字符.
// var reg = /^[abc]$/;
// a 也可以 b 也可以 c 可以 a ||b || c
// 大括号 量词符. 里面表示重复次数
// var reg = /^abc{3}$/; // 它只是让c重复三次 abccc
// console.log(reg.test('abc')); // f
// console.log(reg.test('abcabcabc')); // f
// console.log(reg.test('abccc')); // t
// 小括号 表示优先级
var reg = /^(abc){3}$/; // 它是让abc重复三次
console.log(reg.test('abc')); // f
console.log(reg.test('abcabcabc')); // t
console.log(reg.test('abccc')); // f
在线测试 菜鸟工具
1.5 预定义类
预定义类指的是某些常见模式的简写方式
预定类 | 说明 |
---|---|
\d | 匹配0-9之间的任一数字,相当于[0-9] |
\D | 匹配所有0-9以外的字符,相当于[ ^0-9] |
\w | 匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_] |
\W | 除所有字母、数字和下划线以外的字符,相当于[ ^A-Za-z0-9_] |
\s | 匹配空格(包括换行符、制表符、空格符等),相当于[\t\r\n\v\f] |
\S | 匹配非空格的字符,相当于[ ^\t\r\n\v\f] |
2. 替换
2.1 replace 替换
replace()方法可以实现替换字符串操作,用来替换的参数是一个字符串或是一个正则表达式,返回值是一个替换完毕的新字符串,但只会替换第一个满足条件的字符串。
// 替换 replace
// var str = 'andy和red';
// // var newStr = str.replace('andy', 'baby');
// var newStr = str.replace(/andy/, 'baby');
// console.log(newStr);
var text = document.querySelector('textarea');
var btn = document.querySelector('button');
var div = document.querySelector('div');
btn.onclick = function() {
div.innerHTML = text.value.replace(/激情|gay/g, '**');
}
2.2 正则表达式参数
/表达式/{switch}
switch(也称为修饰符)按照什么样的模式来匹配,有三种值:
- g:全局匹配
- i:忽略大小写
- gi:全局匹配+忽略大小写
ES6
1. ES6 的新增语法
1.1 let关键字
- let声明的变量只在所处于的块级有效
if (true) {
let a = 10;
}
console.log(a) // a is not defined
- 不存在变量提升 (es6之前可以先使用再声明)
console.log(a); // a is not defined
let a = 20;
- 暂时性死区 (块级中使用let声明的变量 会被绑定在块级作用域内 不受外部变量的影响)
var tmp = 123;
if (true) {
tmp = 'abc';
let tmp;
}
var num = 10
if (true) {
console.log(num);
//报错 因为未声明就先使用 此块级作用域中的num与外部num无关
let num = 20;
}
- 防止循环变量变成全局变量
for (let i = 0; i < 2; i++) {}
console.log(i);
经典面试题
- 变量i是全局的,函数执行时输出的都是全局作用域下的i值
var arr = [];
for (var i = 0; i < 2; i++) {
arr[i] = function () { // 将function()赋值给arr[i],并未执行函数function()
console.log(i); // 函数内部未定义i 根据作用域链 向上一级查找
// 全局变量i的值 函数执行时输出的是循环结束后 不满足循环条件的i的值2
}
}
// i=2 for循环结束 开始执行以下语句
arr[0]();
arr[1]();
将var改为let
- 每次循环都会产生一个块级作用域,每个块级作用域中的变量都是不同的(互不影响),函数执行时输出的是自己上一级(循环产生的块级作用域)作用域下的i值
let arr = [];
for (let i = 0; i < 2; i++) {
arr[i] = function () {
console.log(i);
}
}
arr[0]();
arr[1]();
1.2 const关键字
作用:声明常量,常量就是值(内存地址)不能变化的量。
- 具有块级作用域
if (true) {
const a = 10;
}
console.log(a) // a is not defined
- 声明常量时必须赋值
const PI; // Missing initializer in const declaration
- 常量赋值后,值不能修改
const PI = 3.14;
PI = 100; // Assignment to constant variable.
// 基本数据类型 值不可更改
const ary = [100, 200];
ary[0] = 'a';
ary[1] = 'b';
console.log(ary); // ['a', 'b'];
ary = ['a', 'b']; // Assignment to constant variable.
// 对于复杂数据类型 数据结构内部的值可更改 但不能重新赋值(常量值对应的内存地址不可更改)
1.3 let、const、var的区别
- 使用 var 声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象。
- 使用 let 声明的变量,其作用域为该语句所在的代码块内,不存在变量提升。
- 使用 const 声明的是常量,在后面出现的代码中不能再修改该常量的值。
var | let | const |
---|---|---|
函数级作用域 | 块级作用域 | 块级作用域 |
变量提升 | 不存在变量提升 | 不存在变量提升 |
值可更改 | 值可更改 | 值不可更改 |
1.4 解构赋值
ES6中允许从数组中提取值,按照对应位置,对变量赋值。对象也可以实现解构。
数组解构
// 数组解构允许我们按照一一对应的关系从数组中提取值 然后将值赋值给变量
let ary = [1,2,3];
let [a, b, c, d, e] = ary; // 若无对应值 变量的值则为 undefined
console.log(a)
console.log(b)
console.log(c)
console.log(d)
console.log(e)
对象解构
// 对象解构允许我们使用变量的名字匹配对象的属性 匹配成功 将对象属性的值赋值给变量
let person = {name: 'lisi', age: 30, sex: '男'};
// let { name, age, sex } = person; // 变量匹配属性
// console.log(name)
// console.log(age)
// console.log(sex)
let {name: myName} = person; // myName属于别名
// 将name属性对应的值赋给myName变量
console.log(myName)
1.5 箭头函数
ES6中新增的定义函数的方式。
// 箭头函数是用来简化函数定义语法的
const fn = () => {
console.log(123)
}
fn();
函数体中只有一句代码,且代码的执行结果就是返回值,可以省略大括号
// 在箭头函数中 如果函数体中只有一句代码 并且代码的执行结果就是函数的返回值 函数体大括号可以省略
const sum = (n1, n2) => n1 + n2;
const result = sum(10, 20);
console.log(result)
// 在箭头函数中 如果形参只有一个 形参外侧的小括号也是可以省略的
const fn = v => {
alert(v);
}
fn(20)
// 箭头函数不绑定this 箭头函数没有自己的this关键字
// 如果在箭头函数中使用this this关键字将指向箭头函数定义位置中的this
function fn () {
console.log(this);
return () => {
console.log(this)
}
}
const obj = {name: 'zhangsan'};
const resFn = fn.call(obj);
resFn();
箭头函数面试题
var age = 100; // 在window下添加一个age属性
// obj为对象,不能产生作用域,箭头函数实际上被定义在全局作用域下 其this指向window
var obj = {
age: 20,
say: () => {
alert(this.age)
}
}
obj.say();
1.6 剩余参数
剩余参数语法允许我们将一个不定数量的参数表示为一个数组。
// const sum = (...args) => {
// let total = 0;
// args.forEach(item => total += item);
// return total;
// };
// console.log(sum(10, 20));
// console.log(sum(10, 20, 30));
let ary1 = ['张三' , '李四', '王五'];
let [s1, ...s2] = ary1;
console.log(s1) // 张三
console.log(s2) // 数组 ['李四', '王五']
2. ES6 的内置对象扩展
2.1 Array的扩展方法
扩展运算符(展开语法)
// 扩展运算符可将数组拆分成以逗号分隔的参数序列
// let ary = ["a", "b", "c"];
// ...ary // "a", "b", "c"
// console.log(...ary)
// console.log("a", "b", "c")
// 扩展运算符可用于数组合并
// let ary1 = [1, 2, 3];
// let ary2 = [4, 5, 6];
// // ...ary1 // 1, 2, 3
// // ...ary1 // 4, 5, 6
// let ary3 = [...ary1, ...ary2];
// console.log(ary3)
// 合并数组的第二种方法
// let ary1 = [1, 2, 3];
// let ary2 = [4, 5, 6];
// ary1.push(...ary2);
// console.log(ary1)
// 利用扩展运算符可将伪数组转换为真正的数组
var oDivs = document.getElementsByTagName('div');
console.log(oDivs)
var ary = [...oDivs];
ary.push('a');
console.log(ary);
构造函数方法:Array.from()
将类数组或可遍历对象转换为真正的数组,方法还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
// var arrayLike = {
// "0": "张三",
// "1": "李四",
// "2": "王五",
// "length": 3
// }
// var ary = Array.from(arrayLike);
// console.log(ary)
var arrayLike = {
"0": "1",
"1": "2",
"length": 2
}
var ary = Array.from(arrayLike, item => item * 2)
console.log(ary)
实例方法:find()
用于找出第一个符合条件的数组成员,如果没有找到返回undefined
var ary = [{
id: 1,
name: '张三'
}, {
id: 2,
name: '李四'
}];
let target = ary.find(item => item.id == 3);
console.log(target)
实例方法:findIndex()
用于找出第一个符合条件的数组成员的位置,如果没有找到返回-1
let ary = [10, 20, 50];
let index = ary.findIndex(item => item > 15);
console.log(index)
实例方法:includes()
表示某个数组是否包含给定的值,返回布尔值。
let ary = ["a", "b", "c"];
let result = ary.includes('a')
console.log(result)
result = ary.includes('e')
console.log(result)
2.2 String 的扩展方法
模板字符串
ES6新增的创建字符串的方式,使用反引号定义
// 模板字符串中可以解析变量。
let name = `张三`;
let sayHello = `Hello, 我的名字叫${name}`;
console.log(sayHello);
// 模板字符串中可以换行
let result = {
name: "zhangsan",
age: 20
};
let html = `
<div>
<span>${result.name}</span>
<span>${result.age}</span>
</div>
`;
console.log(html);
// 在模板字符串中可以调用函数。
const fn = () => {
return '我是fn函数'
}
let html = `我是模板字符串 ${fn()}`;
console.log(html)
实例方法:startsWith() 和 endsWith()
- startsWith():表示参数字符串是否在原字符串的头部,返回布尔值
- endsWith():表示参数字符串是否在原字符串的尾部,返回布尔值
let str = 'Hello ECMAScript 2015';
let r1 = str.startsWith('Hello'); // t
console.log(r1);
let r2 = str.endsWith('2016'); // f
console.log(r2)
实例方法:repeat()
repeat方法表示将原字符串重复n次,返回一个新字符串。
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
2.3 Set 数据结构
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成 Set 数据结构。
const s = new Set();
Set函数可以接受一个数组作为参数,用来初始化。
const set = new Set([1, 2, 3, 4, 4]);
实例方法
- add(value):添加某个值,返回 Set 结构本身
- delete(value):删除某个值,返回一个布尔值,表示删除是否成功
- has(value):返回一个布尔值,表示该值是否为 Set 的成员
- clear():清除所有成员,没有返回值
const s1 = new Set();
console.log(s1.size)
const s2 = new Set(["a", "b"]);
console.log(s2.size)
const s3 = new Set(["a", "a", "b", "b"]); // 数组去重
console.log(s3.size)
const ary = [...s3];
console.log(ary)
const s4 = new Set();
// 向set结构中添加值 使用add方法
s4.add('a').add('b');
console.log(s4.size)
// 从set结构中删除值 用到的方法是delete
const r1 = s4.delete('c');
console.log(s4.size)
console.log(r1);
// 判断某一个值是否是set数据结构中的成员 使用has
const r2 = s4.has('d');
console.log(r2)
// 清空set数据结构中的值 使用clear方法
s4.clear();
console.log(s4.size);
// 遍历set数据结构 从中取值
const s5 = new Set(['a', 'b', 'c']);
s5.forEach(value => {
console.log(value)
})