如果你是一个入门初学者,对前端的重要知识掌握的不够扎实或间接遗忘,亦或你是现在正在寻找工作,背烂了面试题,却被面试官处处难为到,这篇博文一定对你有很大的帮助,这篇博文主要详细结合案例讲述前端的难点:闭包和继承。 是许多前端大佬拿来反复咀嚼的知识,也是面试官们必问的题了吧。下面结合案例来讲述,希望这篇博文对你能起到较大的帮助。
函数定义过程:
function fn(){
var num = 10;
num++;
console.log(num)
}
函数调用的时候,会在内存中的另外一个空间(执行空间)中进行,当函数执行结束后,执行空间立马销毁:
fn()
如果一个函数中返回了一个复杂类型数据,这个函数中的执行空间就不会被销毁:
function fn(){
return {
}
}
var obj = fn()
执行的时候的过程:
因为栈中变量obj跟执行中间中返回的数据空间保持了引用关系,所以这个fn的执行空间不能销毁,因为一旦销毁后,就没有了obj引用的数据空间了。
作用域嵌套形成的一种js的高级应用场景。
大函数中直接或间接的返回一个小函数,小函数访问大函数中的变量。此时大函数的执行空间不会被销毁了。小函数叫做大函数的闭包函数。
function fn(){
var num = 10;
function fun(){
num++;
return num
}
return fun;
}
var f = fn();
/当一个大函数中,返回一个小函数,小函数中使用了大函数中的变量 - 里面小函数叫做大函数的闭包
function fn(){
var num = 10;
return function(){
num++;
console.log(num);
}
}
var fun = fn()
console.log(fun);
fun()
fun()
fun()
fun()
fun()
fun()
钱包系统 - 存钱、消费、查看余额
var money = 100;
money += 10;
console.log(money);
money -= 20;
定义在全局中的变量,容易被覆盖 - 会污染全局
function fn(){
var money = 100;
return money
}
var m = fn()
console.log(m);
没有办法操作局部的money,无法存钱和消费了
function fn(){
var money = 100;
function fun(num){
money += num
return money
}
return fun
}
var f = fn()
var m = f(30)
console.log(m);
var n = f(40)
console.log(n);
闭包的应用场景 - 通常会使用闭包解决 在循环中执行异步代码的问题
for(var i=1;i<=3;i++){
setTimeout(function(){
console.log(i);
},1000*i)
}
for(var i=1;i<=3;i++){
function fn(i){
// var i = 1/2/3
setTimeout(function(){
console.log(i);
},1000*i)
}
fn(i)
}
for(var i=1;i<=3;i++){
var fn = (function(i){
return function(){
setTimeout(function(){
console.log(i);
},i*1000)
}
})(i)
fn()
}
tab切换
var oLis = document.querySelectorAll('li')
var oDivs = document.querySelectorAll('div')
for(var i=0;i<oLis.length;i++){
(function(i){
oLis[i].onclick = function(){
// 将所有的li的active去掉
for(var j=0;j<oLis.length;j++){
oLis[j].className = '';
oDivs[j].className = '';
}
this.className = 'active';
// 让i能用
console.log(i);
}
})(i)
}
星星评分:
DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>documenttitle>
head>
<body>
<img src="./images/rank_3.gif" alt=""><img src="./images/rank_4.gif" alt="">
<img src="./images/rank_3.gif" alt=""><img src="./images/rank_4.gif" alt="">
<img src="./images/rank_3.gif" alt=""><img src="./images/rank_4.gif" alt="">
<img src="./images/rank_3.gif" alt=""><img src="./images/rank_4.gif" alt="">
<img src="./images/rank_3.gif" alt=""><img src="./images/rank_4.gif" alt="">
body>
<script>
var oImgs = document.querySelectorAll('img')
for(var i=0;i<oImgs.length;i++){
oImgs[i].onmouseover = fn(i)
}
function fn(i){
return function(){
for(var j=0;j<=i;j++){
if(j%2){
oImgs[j].src = './images/rank_2.gif';
}else{
oImgs[j].src = './images/rank_1.gif';
}
}
for(var j=i+1;j<oImgs.length;j++){
if(j%2){
oImgs[j].src = './images/rank_4.gif';
}else{
oImgs[j].src = './images/rank_3.gif';
}
}
}
}
script>
html>
闭包的作用:
缺点:有一个不会被销毁的内存空间,容易造成内存泄漏。
闭包的应用案例:循环中绑定事件 - 星星评分
语法糖:就是利用这个语法的一种场景,且使用起来方便, 但是看起来不舒服。
闭包有一个语法糖是获取器和设置器,分别是get和set。
正常的写法和用法:
function fn(){
var num = 100;
return {
getNum(){
return num;
},
setNum(val){
num = val
}
}
}
var res = fn();
console.log(res.getNum()) // 100
res.setNum(500)
console.log(res.getNum()) // 500
语法糖写法:
function fn(){
var num = 100;
return {
get num(){
return num;
},
set num(val){
num = val
}
}
}
var res = fn();
console.log(res.num) // 100
res.num = 500
console.log(res.num) // 500
get方法想要做的事情就是访问num的值,set方法想要做的事情就是设置num的值
换语法糖写之后,num作为了res对象的属性,直接可以访问,当访问num值的时候,其实就是在调用get方法,当给num直接赋值的时候,其实就是在调用set方法。
函数柯里化其实就是利用闭包将函数的多个参数拆分成多个函数使用,方便模块化开发,例:
function fn(reg,username){
return reg.test(username)
}
这是一个验证用户名是否正确的函数,调用方式如下:
var reg = /^[a-zA-Z]\dw{3,11}$/;
var res = fn(reg,'cuihua')
var res1 = fn(reg,'ruhua')
从上面重复使用的代码中可以看出来,每次使用的reg都是一样的,且在模块化开发中,不建议将变量暴露出来,暴露的都是一个一个的方法,也就是函数,所以讲上面的函数改写:
function fn(reg){
return function(username){
return reg.test(username)
}
}
使用方式如下:
var test = fn(/^[a-zA-Z]\dw{3,11}$/);
var res = test('cuihua')
var res1 = test('ruhua')
这种使用方式,没有将变量暴露在外,而是将函数内部的函数暴露在外面,更加符合模块化的要求。
这就是函数的柯里化。
案例:正则验证的模块化封装
function fn(i){
return function(n){
console.log(n + (--i))
}
}
var f = fn(2)
f(3)
fn(4)(5)
fn(6)(7)
f(8) ---结果是 8
如下三个构造函数:
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log('各种声音')
}
function Cat(eye) {
this.eye = eye
}
Cat.prototype.xingwei = function () {
console.log('抓老鼠')
}
function Bird(body) {
this.body = body
}
Bird.prototype.fly = function () {
console.log('飞')
}
const c = new Cat('蓝黄')
console.log(c)
const b = new Bird('翅膀')
console.log(b)
从下面两个实例对象中可以看出,每个对象都有自己的属性和方法,不带有Animal的属性和方法,但Cat和Bird又希望能使用到Animal的属性和方法。
当多个构造函数需要使用一些共同的属性和方法时候,就需要将将共同的属性和方法单独封装在一个构造函数中,方便多个构造函数继承使用。
继承:就是让子构造函数的实例对象能使用父构造函数的属性和方法。
父构造函数叫父类,子构造函数叫子类。继承的目的,就是让子类拥有父类的属性和方法。
通过概念对象的原型链结构来实现继承,因为对象默认能访问到原型对象的属性和方法
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log('各种声音')
}
function Cat(eye) {
this.eye = eye
}
// 改变Cat的原型
Cat.prototype = new Animal('猫',5)
Cat.prototype.xingwei = function () {
console.log('抓老鼠')
}
var c = new Cat('眼睛')
console.log(c)
优点:只要处在原型上的方法和属性都能使用
缺点:一个构造函数的实例创建需要在两个地方传递参数,且同样是属性,不在同一个地方显示
利用借用函数,将父构造函数中的this改成子构造函数中的this,让子构造函数具备跟父构造函数同样的属性。
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log('各种声音')
}
function Cat(name,age,eye) {
// 使用借用函数调用父构造函数
Animal.call(name,age)
this.eye = eye
}
Cat.prototype.xingwei = function () {
console.log('抓老鼠')
}
var c = new Cat('猫',5,'眼睛')
console.log(c)
优点:在同一个地方传递参数了,且属性都放在了一起。
缺点:父构造函数原型上的方法继承不了。
组合继承是将原型继承和借用函数继承同时使用。
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log('各种声音')
}
function Cat(name,age,eye) {
// 使用借用函数调用父构造函数
Animal.call(name,age)
this.eye = eye
}
// 改变Cat的原型
Cat.prototype = new Animal()
Cat.prototype.xingwei = function () {
console.log('抓老鼠')
}
var c = new Cat('猫',5,'眼睛')
console.log(c)
优点:
缺点:当给子类添加方法的时候, 实际上是添加在了父类的实例身上
拷贝继承是利用for in循环能将原型中的属性和方法也遍历出来的特性,将父对象中属性和方法遍历绑定到子对象的原型上。
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log('各种声音')
}
function Cat(name,age,eye) {
// 在子构造函数中实例化父构造函数
var a = new Animal(name,age)
// 遍历父对象
for(var attr in a){
// 给子对象的原型上添加父对象中的属性和方法
Cat.prototype[attr] = a[attr]
}
this.eye = eye
}
Cat.prototype.xingwei = function () {
console.log('抓老鼠')
}
var c = new Cat('猫',5,'眼睛')
console.log(c)
优点:
缺点:
将父构造函数在子构造函数中实例化,然后在子构造函数中返回实例出来的对象。
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log('各种声音')
}
function Cat(name,age,eye) {
// 在子构造函数中实例化父构造函数
var a = new Animal(name,age)
// 给这个对象中添加属性
a.eye = eye;
return a;
}
Cat.prototype.xingwei = function () {
console.log('抓老鼠')
}
var c = new Cat('猫',5,'眼睛')
console.log(c)
这种继承没有了自己的原型,原型完全是父构造函数的继承,改变自己原型后,自己原本原型上的方法又无法使用。
function Cat(name,age,eye) {
// 在子构造函数中实例化父构造函数
var a = new Animal(name,age)
// 改原型
Cat.prototype = a.__proto__;
// 给这个对象中添加属性
a.eye = eye;
return a;
}
Cat.prototype.xingwei = function () {
console.log('抓老鼠')
}
var c = new Cat('猫',5,'眼睛')
console.log(c) // 对象中就没有了xingwei这个方法了
寄生继承的另一种方式修改:
function Cat(name,age,eye) {
// 在子构造函数中实例化父构造函数
var a = new Animal(name,age)
// 改原型
Cat.prototype = a.__proto__;
// 给这个对象中添加属性
a.eye = eye;
return a;
}
Animal.prototype.xingwei = function () {
console.log('抓老鼠')
}
var c = new Cat('猫',5,'眼睛')
console.log(c) // 对象中又有了xingwei这个方法了
但这种继承方式,将父构造函数的原型修改了,以后的继承,都会带有这个方法。
优点:
缺点:
寄生组合继承是合并了寄生继承和组合继承以及借助了第三方独立的构造函数而完成。
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log('各种声音')
}
function Cat(eye) {
this.eye = eye;
}
Cat.prototype.xingwei = function () {
console.log('抓老鼠')
}
// 为了保护全局变量不被污染,使用自调用函数
(function(){
// 借助第三方构造函数
function Temp(name,age){
// 再使用借用函数继承
Animal.call(this,name,age)
}
// 先原型继承
Temp.prototype = new Animal()
// 原型继承后的实例对象,原型上有父构造函数的属性,原型的原型上有父构造函数的方法
// 再加借用函数继承后,实例对象中拥有父类的属性,方法在原型的原型上
var t = new Temp('猫',5);
console.log(t)
// 将得到的实例对象作为子类的原型
Cat.prototype = t;
})()
// 然后Cat的实例对象中,需要有的就全有了
var c = new Cat('眼睛')
console.log(c)
Animal经过借用函数继承后,属性已经有了,需要的只剩下原型中的方法了,所以对原型进行寄生即可,不用实例化Animal了,简化如下:
(function(){
// 借助第三方构造函数
function Temp(name,age){
// 借用函数继承
Animal.call(this,name,age)
}
// 原型寄生
Temp.prototype = Person.prototype
// 寄生原型
Cat.prototype = new Temp('猫',5)
})()
var c = new Cat('眼睛')
console.log(c)
es6提供了类的语法和继承的语法:
class Animal{
constructor(name,age){
this.name = name
this.age = age
}
say(){
console.log('各种声音')
}
}
class Cat extends Animal(){
constructor(name,age,eye){
super(name,age) // 相当于在调用父类的constructor
this.eye = eye
}
xingwei () {
console.log('抓老鼠')
}
}
var c = new Cat('猫',5,'眼睛')
console.log(c)
喜欢的欢迎收藏,记得点赞关注哦!