JavaScript学习笔记(四)---闭包、递归、柯里化函数、继承、深浅拷贝、设计模式
1. 匿名函数的使用场景
匿名函数: function关键字后面没有标识符的时候,它就是一个匿名函数。
-
绑定事件
document.onclick = function(){ }
-
作为等号的右值
let fun = function(){ } setInterval(fun,2000);
-
可以作为函数的参数,回调函数
setInterval(function(){ },2000);
匿名函数的本质就是一个对象----->对象如何使用,匿名函数就可以如何使用
-
函数也可以作为函数的返回值,学习闭包的前置条件
function fn(){ return function(){ } }
2.自运行
自运行:一个匿名函数在定义完自动运行,学习闭包的前置条件
let fun = function(){
console.log("李峰");
}
fun == function () {
console.log("李峰");
}
fun() == function(){
console.log("李峰");
}()
// 思想:实现了函数定义时自动调用,但语法无法实现
function(){
console.log("李峰");
}();
匿名函数自运行的写法:
-
第一种
-
(function () { console.log("李峰"); }());
-
第二种
(function () { console.log("王聪"); })();
-
第三种:通过运算符实现
! function(){ console.log("翻翻"); }();
-
第四种:通过关键字实现
void function(){ console.log("翻翻"); }();
3.闭包
3.1前提:
全局变量:全局变量会降低函数的独立性
var count = 0;
function fun() {
console.log(++count);
}
fun();
fun();
fun();
局部变量:函数执行外,局部变量被销毁
function fun() {
var count = 0;
console.log(++count);
}
fun();
fun();
fun();
3.2闭包
闭包的概念:函数嵌套函数,被嵌套的函数称为闭包函数
闭包的作用:在一个函数体外,使用了函数的局部变量(会保证私有属性的安全性)
function f1() {
var count = 0;
var f2 = function () {
++count;
return count;
}
return f2;
}
let f = f1(); //f1() == f2() == f
console.log(f()); //f() == f2()
console.log(f());
console.log(f());
//变形
function f1() {
var count = 0;
return function () {
return ++count;
}
}
let f = f1();
console.log(f());
console.log(f());
console.log(f());
// 完全体
let f = (function f1() {
var count = 0;
return function () {
return ++count;
}
}());
console.log(f());
console.log(f());
console.log(f());
为什么可以在f1体外使用count?
闭包的实现:在主函数(f1)中定义内部变量(count),及子函数(f2),在子函数(f2)中使用主函数(f1)中定义内部变量(count),将子函数(f2)作为主函数(f1)的返回值,在外界通过全局变量f绑定了f1的返回值f2,从而延长了f2的生命周期,使得count可以在f1外界使用。
特点:1.函数嵌套函数,2.函数内部可以引用外部的参数和变量,3.参数和变量不会被垃圾回收机制回收
用处:1.常驻内存会增大内存的使用量,2.读取函内部的变量,3.这些变量的值始终保持在内存中,不会在外层函数调用后被自动清除
优点:1.变量长期驻扎在内存中,2.避免全局变量的污染,3.私有成员的存在
缺陷:打破了垃圾回收机制,延长局部变量的生命周期,使用闭包可能会造成内存泄漏
闭包的案例:
// 1.分析程序
function outerF() {
var t = 0;
function innerF() {
t++;
console.log(t);
}
return innerF;
}
var f = outerF();
// 2.通过闭包获取元素下标
let oLis = document.querySelectorAll("li");
for (var i = 0; i < oLis.length; i++) {
// 通过添加自定义属性
// oLis[i].index = i;
// oLis[i].onclick = function () {
// console.log(this.index);
// };
// 通过闭包,具有块级作用域
(function (i) {
oLis[i].onclick = function () {
console.log(i);
}
}(i));
}
// 3.通过闭包实现
// add(1)(2)(3)
// function add(x, y, z) {
// return x + y + z;
// }
// console.log(add(1, 2, 3));
function add(x) {
return function (y) {
return function (z) {
return x + y + z;
}
}
}
console.log(add(1)(2)(3));
4.函数对象的三种定义方式
第一种:
function fun() {
console.log("hhhh");
}
fun();
第二种:
let fun1 = function () {
console.log("我爱学习");
}
fun1();
第三种:let fun2 = new Function([参数列表],代码块);
// let fun2 = new Function([参数列表],代码块);
let fun2 = new Function("console.log('ffff')");
fun2();
let fun3 = new Function("a", "b", "console.log(a+b)");
fun3(1, 2);
function f1() {// new Function(...)
console.log("李峰");
}
function f2() {// new Function(...)
console.log("李峰");
}
console.log(f1 == f2);//false
上述代码中结果为false,两个函数在堆空间中new出的空间地址不一样
5.this
函数的内置对象:在函数体内可以使用的对象
this:
- 与事件体连用,触发该事件的元素本身
- 与普通方法连用,调用该函数的对象
- 与构造方法连用,代表new出来的对象
- 与箭头函数连用,其父元素的前缀
6.arguments
arguments:
作用:围绕着不定参数
arguments:获取实参的伪数组(函数接收实参的伪数组)
function f() {
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
f(1, true, 'jjj');
不定参数的设定:
function fun() {
for (let i = 0; i < arguments.length; i++) {
if (typeof arguments[i] == "number") {
console.log("数字相关操作");
} else if (typeof arguments[i] == "string") {
console.log("字符串相关操作");
} else if (typeof arguments[i] == "boolean") {
console.log("布尔相关操作");
}
}
}
fun("heihe", true);
7.arguments.callee
arguments.callee:代表当前的函数对象本身
// let fun = function () {
// console.log(arguments.callee);
// }
// fun();
function fun(n) {
let c;
if (n == 1) {
c = 10;
} else {
c = arguments.callee(n - 1) + 2;
}
return c;
}
8.递归
递归:一个函数直接或者间接的调用自己本身
let count = 0;
function fun() {
console.log(++count);
fun();
}
fun();
上述代码:结果是电脑栈区内存被占满后导致栈溢出
递归的本质就是函数的嵌套调用
作用:将长代码变短,去掉代码的冗余
应用:
// 有五个小朋友一次排列,问第五位小朋友的年纪,他说他比第四位大两岁,
// ……
// 第一位小朋友10岁
function fun(n) {
let age = 10;
if (n == 1) {
age = 10;
} else {
age = fun(n - 1) + 2;
}
return age;
}
console.log(fun(5));
9.prototype
function Student(id, name) {
this.id = id;
this.name = name;
// 核心问题:行为方法不应该属于每一个实例化对象
// 它们应该属于整个类族,且只有一份
// this.study = function () {
// console.log("study");
// }
// this.eat = function () {
// console.log("eat");
// }
}
原型对象:prototype
原型对象是函数对象(构造函数)的一个属性,它是用来保存所有实例对象共享的属性和方法的
Student.prototype.study = function () {
// this在普通函数中表示调用该函数的对象
console.log(this.name + "study");
}
Student.prototype.eat = function () {
console.log(this.name + "eat");
}
Student.prototype.teacher = "大黄";
Student.prototype.teacher = "小黄";
let s1 = new Student(1, "翻翻");
let s2 = new Student(2, "王博");
s1.study();
s2.study();
// 实例化对象是不可以改变原型对象的属性或方法
// 如果实例化对象修改了该属性,效果等价于为自身添加了一个新的属性
s1.teacher = "曹量";
console.log(s1.teacher, s2.teacher);
实例化对象是不可以改变原型对象的属性或方法
如果实例化对象修改了该属性,效果等价于为自身添加了一个新的属性。
为什么实例化对象可以访问所有的属性和方法?
实例化对象可以直接访问所有的属性和方法,每个实例化对象都有一个_proto_
属性,该属性指向类的原型对象,所以实例化对象可以访问原型对象上的属性或者方法。
原型对象案例:
<script>
Array.prototype.max = function () {
// this在普通函数中指调用该函数的对象
let x = this[0];
for (let i = 0; i < this.length; i++) {
if (x < this[i]) {
x = this[i];
}
}
return x;
}
let arr = [1, 2, 13, 4, 15];
console.log(arr.max());
</script>
10.apply和call区别
需求:不同的类拥有类似的方法,能否复用方法
核心思想,如何改变eat方法中的this指向?
apply和call都是用来改变函数对象this指向的函数
函数对象.apply(被修改的this指向,[函数对象参数1,参数2……])
函数对象.call(被修改的this指向,函数对象参数1,参数2……)
function Monkey(name) {
this.name = name;
}
function Snake(name) {
this.name = name;
}
function eat(food1, food2) {
console.log(this.name + " " + food1 + " " + food2);
}
// 核心思想,如何改变eat方法中的this指向?
// apply和call都是用来改变函数对象this指向的函数
// 函数对象.apply(被修改的this指向,[函数对象参数1,参数2……])
// 函数对象.call(被修改的this指向,函数对象参数1,参数2……)
let m = new Monkey("村五块");
eat.call(m, "葡萄", "西瓜");
eat.apply(m, ["葡萄", "西瓜"]);
// apply,call,bind的异同?
// 1.都是用来改变函数的this指向
// 2.bind用来改变匿名函数(事件体)
// 3.apply和call通常用来改变有名函数的this指向
// 4.apply的第二个参数为数组,call按顺序填写
// 5.bind没有直接调用函数,等价于创建了一个新的函数对象
let f = eat.bind(m, "葡萄", "西瓜");
f();
apply,call,bind的异同?
- 都是用来改变函数的this指向
- bind用来改变匿名函数(事件体)
- apply和call通常用来改变有名函数的this指向
- apply的第二个参数为数组,call按顺序填写
- bind没有直接调用函数,等价于创建了一个新的函数对象
11.柯里化函数
柯里化函数:一个包含一个参数并且返回一个函数的函数
作用:柯里化实际是把简单的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更多的自由度。而这里对于函数参数的自由处理,正是柯里化的核心所在。柯里化本质上是降低通用性,提高适用性。
案例:
// 案例
// function checkReg(reg, str) {
// return reg.test(str);
// }
// 用户名
// console.log(checkReg(/^\w{6,18}$/, "heihei"));
// console.log(checkReg(/^\w{6,18}$/, "123"));
// console.log(checkReg(/^\w{6,18}$/, "laowang"));
// 密码
// console.log(checkReg(/^.{6,}$/, "123"));
// console.log(checkReg(/^.{6,}$/, "11111111"));
// console.log(checkReg(/^.{6,}$/, "123"));
// 改写为柯里化函数
function checkReg(reg) {
return function (str) {
return reg.test(str);
}
}
// 生成工具函数,判断用户名
let f1 = checkReg(/^\w{6,18}$/);
console.log(f1("heihei"));
console.log(f1("123"));
console.log(f1("laowang"));
let f2 = checkReg(/^.{6,}$/);
console.log(f1("heihei"));
console.log(f1("123"));
console.log(f1("laowang"));
12.继承
作用:将父类的属性和方法派生给子类,子类可以直接使用父类派生的属性和方法,从而提高代码的复用性,并且子类还可以添加新的属性和方法。
原型继承、借用构造方法继承、混合继承、ES6继承
12.1原型继承
-
原型继承:通过原型对象实现继承
-
语法规则:子类的原型对象等于父类的实例化对象;子类.prototype = new 父类( );
-
为什么子类的对象可以访问所有的属性和方法?
原型链思想:向上找
子类对象可以直接访问new出来的属性,通过自己的
_proto_
访问原型对象的方法,在通过子类原型对象执行父类的实例化对象便可以访问父类的属性,通过父类实例化对象的_proto_
属性,可以访问父类原型对象上的属性和方法。 -
多层继承关系:两两类之间的关系
-
原型继承的缺陷:1.无法在子类对象构造时,初始化父类派生给子类的属性;2.必须先实现继承关系,然后再对子类对象添加新的原型方法,并且一旦继承关系实现,原型对象的指向就不能再改变。
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function () {
console.log("Animal eat");
}
function Human(id) {
this.id = id;
}
// 实现了Human继承 Animal的关系
Human.prototype = new Animal("赵四");
Human.prototype.coding = function () {
console.log("Huamn coding");
}
let h = new Human(9576);
h.name = "六年";
console.log(h.name);
h.eat();
console.log(h.id);
h.coding();
多层关系继承
<script>
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function () {
console.log("Animal eat");
}
function Human(id) {
this.id = id;
}
Human.prototype = new Animal("老王");
Human.prototype.makeTools = function () {
console.log("Human makeTools");
}
function Student(score) {
this.score = score;
}
Student.prototype = new Human(9657);
Student.prototype.study = function () {
console.log("Student study");
}
let s = new Student(100);
console.log(s.name, s.id, s.score);
s.eat();
s.study();
s.makeTools();
</script>
原型继承的缺陷:
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function () {
console.log("Animal eat");
}
function Huamn(id) {
this.id = id;
}
Human.prototype = new Animal("赵四");
Human.prototype.coding = function () {
console.log("Human coding");
}
let h = new Human(9527);
h.name = "锋锋";
console.log(h.name);
h.coding();
12.2借用构造方法继承
- apply和call继承,借用构造方法
- 作用:可以在子类对象构造时,初始化父类派生给子类的属性
- 缺陷:原型对象的属性无法继承
function Human(name, id) {
this.name = name;
this.id = id;
this.eat = function () {
console.log("eat");
}
}
// Human.prototype.eat = function () {
// console.log("eat");
// }
function Student(name, id, score) {
// 模拟了属性的继承
// Human.apply(this, [name, id]);
Human.call(this, name, id);
this.score = score;
}
let s = new Student("老王", "9572", 10086);
console.log(s);
s.eat();
12.3混合继承
- 混合继承:通过原型继承和借用构造方法继承,实现完善的继承方法
- 属性通过借用构造方法继承
- 原型对象通过原型继承
function Human(name, id) {
this.name = name;
this.id = id;
}
Human.prototype.eat = function () {
console.log("Animal eat");
}
function Student(name, id, score) {
// 属性的继承
Human.call(this, name, id);
this.score = score;
}
// 原型的继承
Student.prototype = new Human();
Student.prototype.study = function () {
console.log("study");
}
let s = new Student("小王", "666", 100);
console.log(s.name, s.id, s.score);
s.eat();
s.study();
12.4ES6继承
class 子类 extends 父类{ }
<script>
class Huamn {
constructor(name, id) {
this.name = name;
this.id = id;
}
eat() {
console.log("Huamn eat");
}
}
// class 子类 extends 父类{ }
class Student extends Huamn {
constructor(name, id, score) {
// 借用父类构造方法
// 必须放在第一行
super(name, id);
this.score = score;
}
study() {
console.log("study");
}
}
let s = new Student("dangn", "888", 89);
console.log(s);
s.eat();
s.study();
</script>
13.引用类型的判断
语法格式:对象 instanceof 类型名:返回布尔值
面试题
typeof和instanceof的异同?
- 都是用来判断数据类型的关键字
- typeof用来判断内置基本类型,number,string,boolean,如果用来判断引用类型则返回object
- instanceof判断引用类型,返回的结果是布尔值,且满足类型兼容规则
// 对象 instanceof 类型名:返回布尔值
let arr = [1, 2, 3, 4];
console.log(arr instanceof Array);
console.log(arr instanceof String);
console.log(arr instanceof Object);
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function () {
console.log("Animal eat");
}
function Human(id) {
this.id = id;
}
// 实现了Huamn继承Animal的关系
Human.prototype = new Animal("赵四");
Human.prototype.coding = function () {
console.log("Human coding");
}
let a = new Animal("孙悟空");
console.log(a instanceof Animal);//true
console.log(a instanceof Human);//false
let h = new Human(000);
console.log(h instanceof Human);//true
console.log(h instanceof Animal);//true
14.深浅拷贝
前置问题:内置基本类型和引用类型在内存中存储的区别
内置基本类型:只有一块空间,栈空间,存储的是数值
引用类型:有两块空间,一块栈空间,存储堆空间new出来的地址,一块堆空间,存储数值
// 内置基本类型:只有一块空间,栈空间,存储的是数值
let a = 123;
// 引用类型:有两块空间,一块栈空间,存储堆空间new出来的地址
// 一块堆空间,存储数值
let arr = [1, 2, 3];
let [a, b] = [123, 456];
let t;
t = a;
a = b;
b = t;
function swap(x, y) {
let t;
t = x;
x = y;
y = t;
}
swap(a, b);
console.log(a, b);
// ----------------------
let arr = [1, 2];
function swap(a) {
t = a[0];
a[0] = a[1];
a[1] = t;
}
swap(arr);
//里面值发生改变因为传进去的虽然是引用地址但是函数里面进行的值之间的交换
console.log(arr);
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
function swap(a1, a2) {
let t;
t = a1;
a1 = a2;
a2 = t;
}
// 值未发生改变,因为实参传进去的是地址函数中也只是进行了地址的交换
// 函数执行结束空间销毁,最终值未发生改变
swap(arr1, arr2);
console.log(arr1, arr2);
拷贝:由已有对象初始化一个新的对象
深浅拷贝只针对引用类型,与内置基本类型无关
浅拷贝:在拷贝的过程中,只赋值但是并不开辟空间,两个对象共享同一块空间,其中一个发生改变会影响另一个
深拷贝:开辟空间且赋值
// 拷贝:由已有对象初始化一个新的对象
// let a = 123;
// let b = a;
// 深浅拷贝只针对引用类型,与内置基本类型无关
// let arr = [1, 2, 3];
// 浅拷贝:在拷贝的过程中,只赋值但是并不开辟空间,
// 两个对象共享同一块空间,其中一个发生改变会影响另一个
// let arr1 = arr;
// arr[0] = "翻翻";
// console.log(arr);
// console.log(arr1);
// 深拷贝:开辟空间且赋值
// let arr = [1, 2, 3];
// let arr1 = [];
// for (let i = 0; i < arr.length; i++) {
// arr1.push(arr[i]);
// }
// arr1[2] = "哈哈哈";
// console.log(arr);
// console.log(arr1);
深拷贝的案例
function Student(id, name) {
this.id = id;
this.name = name;
}
Student.prototype.copy = function () {
let item = new Student(this.id, this.name);
return item;
}
let s1 = new Student(1, "凡凡");
let s2 = s1.copy();
15.设计模式
设计模式是什么?
背景:目前企业级分布式软件开发普遍采用面向对象的方法,OOD(面向对象设计)直接导致了设计模式的发展;开发面向对象的软件是困难的,而开发可复用的面向对象的软件更难;采用设计模式使设计和代码具有良好的可维护性、可复用性和可升级性。
定义:模式是一个上下文中,对一个问题的解决方案。即模式的四要素:名字、上下文、问题和解决方案。
为什么使用设计模式:
- 尽量用一种标准的方式描述设计经验
- 为设计者提供一种通用的语言
- 增加复用性,减少设计的多样性
- 增强设计变更的灵活性
- 提高设计文档的质量
- 增强设计的可理解性
15.1单例模式(必须掌握)
单例模式指的是一个类只能有一个实例,这样的类被称为单例类,或者单态类
单例类的特点:
- 单例类只可有一个实例
- 它必须直接创立这唯一的一个实例
- 它必须给所有其他的类提供自己这一实例
作用:更加方便的在项目中传递数据
-
通过类的自定义属性创建单例模式
function King(name) { if (King.unique == undefined) { this.name = name; // 这里的this指的是实例化对象 King.unique = this; } else { return King.unique; } } let k1 = new King("秦始皇"); // 缺陷:外界改变自定义属性,则打破单例模式 King.unique = undefined; let k2 = new King("李世明"); console.log(k1 == k2);
-
闭包实现单例模式
let king = (function (name) { var unique; function King(name) { this.name = name; } if (unique == undefined) { unique = new King(name); } return function () { return unique; } }("乾隆")); let k1 = king(); console.log(k1); let k2 = king(); console.log(k1 == k2);
15.2代理模式
定义:是把对一个对象的访问,交给另一个代理对象来操作。原因:A对象无法执行某个自己的功能,而让别人执行
// 这个例子模拟的是妈妈委托护士去喂养孩子哄孩子睡觉
class Nurse {
constructor(name) {
this.name = name;
}
takeBabyEat() {
console.log(this.name + ":takeBabyEat");
}
takeBabySleep() {
console.log(this.name + ":takeBabySleep");
}
}
class Mother {
// 将代理对象作为参数传递
constructor(nurser) {
this.nurser = nurser;
}
takeBabyEat() {
this.nurser.takeBabyEat();
}
takeBabySleep() {
this.nurser.takeBabySleep();
}
}
let nurser = new Nurse("林颖");
let m = new Mother(nurser);
m.takeBabyEat();
m.takeBabySleep();
15.3工厂模式
工厂模式:就是用工厂的思路,创建对象。工厂是造产品的。现在用工厂来造对象。即一个工厂可以制造很多种类型的对象,这些对象一般都具有共同的父类,即相似的类。
工厂模式是我们最常用的实例化对象模式,是用工厂方式代替new操作的一种模式。
为什么使用工厂模式?
1.对象的构建十分复杂。处理大量具有相同属性的对象,使用一个类(通常为单位)来批量生成实例。
2.需要依赖具体环境创建不同实例
通过一种方法创建不同的对象,该方法就是工厂。
案例:
function Factory(role) {
function Student() {
this.view = ["考试", "测评", "请假"];
}
function Teacher() {
this.view = ["班级列表", "考试页面", "批阅结果"];
}
function Boss() {
this.view = ["为所欲为"];
}
switch (role) {
case "student":
return new Student();
case "teacher":
return new Teacher();
case "boss":
return new Boss();
}
}
let stu = Factory("student");
let t = Factory("teacher");
let boss = Factory("boss");
console.log(stu);
console.log(t);
console.log(boss);
15.4组合模式
组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构
用小的子对象构造更大的父对象,而这些子对象也由更小的子对象构成。
class Birthdy {
constructor(y, m, d) {
this.y = y;
this.m = m;
this.d = d;
}
showValue() {
console.log(this.y, this.m, this.d);
}
}
class Student {
constructor(id, name, bir) {
this.id = id;
this.name = name;
this.bir = bir;
}
showValue() {
console.log(this.id, this.name);
this.bir.showValue();
}
}
let bir = new Birthdy('2022', '9', '16');
let s = new Student(1, "老王", bir);
s.showValue();
应用场景:组合模式可以在需要针对“树形结构”进行操作的应用中使用,
例如扫描文件夹,渲染网站导航结构等等。
扫描文件夹
// 文件
class File {
constructor(name) {
this.name = name;
}
scan() {
console.log(this.name);
}
}
// 文件夹
class Folder {
constructor(name) {
this.name = name;
this.myfile = [];
}
scan() {
console.log(this.name);
for (let item of this.myfile) {
item.scan();
}
}
add(file) {
this.myfile.push(file);
}
}
let folder1 = new Folder("folder1");
let folder2 = new Folder("folder2");
let folder3 = new Folder("folder3");
folder1.add(folder2);
folder1.add(folder3);
let file1 = new File("file1");
let file2 = new File("file2");
let file3 = new File("file3");
folder2.add(file1);
folder2.add(file2);
folder3.add(file3);
folder1.scan();
15.5观察者模式(掌握)
观察者模式:发布订阅者模式
观察者关注发布者,当发布者发送消息时,所有观察者都会收到消息。
分为两种角色,双方约定一个函数
观察者:写函数定义
发布者:写函数调用
let sub1 = {
// 预定的函数
// what是发布者传递的信息
update: function (what) {
console.log("sub1:" + what);
},
update1: function () {
}
}
let sub2 = {
// 预定的函数
// what是发布者传递的信息
update: function (what) {
console.log("sub2:" + what);
}
}
let sub3 = {
// 预定的函数
// what是发布者传递的信息
update: function (what) {
console.log("sub3:" + what);
}
}
// 发布者一定世道自己的订阅者
class Dep {
constructor(subs) {
this.subs = subs;
}
infor(what) {
for (let i = 0; i < this.subs.length; i++) {
this.subs[i].update(what);
}
}
}
let d = new Dep([sub1, sub2, sub3]);
d.infor("起床");