JavaScript 学习笔记 十二

71 阅读9分钟

twelve

一:原型与原型链的阶段性理解

个人阶段性理解:原型与原型链,说来说去,其最重要的作用,就是继承与复用。

二:call apply 阶段性理解

个人阶段性理解:call()apply() 方法,展开如下:

  • call() 方法:
    • 用法: fn.call(thisArg, arg1, arg2 ...argN)
    • 功能:调用函数 fn ,同时允许动态修改函数中的上下文的 this 指向;
    • 必选参数:thisArg ,一般常用的实参就是 this 或 对象;
    • 可选参数:arg1, arg2 ...argN ,作为函数的形参按顺序传入。
  • apply() 方法:
    • 用法: fn.apply(thisArg, [argsArray])
    • 功能:调用函数 fn ,同时允许动态修改函数中的上下文的 this 指向;
    • 必选参数:thisArg ,一般常用的形参就是 this 或 对象;
    • 可选参数:[argsArray] ,接收一个数组(或类数组对象),该数组中的元素将作为函数的形数传入。
  • 相同之处:
    • 改变this的指向:这两个方法都可以改变函数运行时this的指向;
    • 借用:它们常被用来借用其他对象的属性和方法;
    • 模拟继承:虽然不是真正的继承,它们可以用于在构造器函数中借用另一个构造器函数的方法,实现一定程度的继承效果。
  • 不同之处:
    • call:第二项参数需要一个接一个的传入;
    • apply:第二项参数作为一个数组传入。

代码范例一:验证上述内容的小部分

//验证 apply() 方法是否有类似原型链上的继承功能
Teacher.prototype.wife = 'Ms.Liu'; // 在构造函数上的原型中挂载一个属性与值。
function Teacher(name, mSkill){ // 声明一个自定义构造函数,并定义形参
    this.name = name; //定义初始化实例对象的属性与值
    this.mSkill = mSkill; //同上
}
function Student(name, mSkill, age, major){ // 声明一个自定义构造函数,并定义形参
    Teacher.apply(this, [name, mSkill]); //apply() 方法的应用
    //apply() 方法的第二个参数,可以是临时数组,也可以在函数外部自定义一个永久数组。
    //从包裹的函数的形参中,组成一个临时数组。
    this.age = age; //定义初始化实例对象的属性与值
    this.major = major; //同上
}
var student = new Student( //实例化对象,并传实参
	'Mr.Zhang', 'JS/JQ', 18, 'Computer'
)

console.log(student); //打印实例化对象
//Print Result:► Student {name: 'Mr.Zhang', mSkill: 'JS/JQ', age: 18, major: 'Computer'}

//尝试能否访问 Teacher 构造函数上的原型对象中的属性与值
//console.log(student.wife) 
//Print Result:undefined 
//Explanation:由结果可知,apply() 方法是没有类似原型链上的继承功能,通过打印 student 对象可以知道,apply() 方法只是实现了一定程度的继承效果。

三:原型链继承关系中的问题

3.1 观察两种数据类型

在 JavaScript 中,原型链继承关系中的继承数据可以分成两类:在继承时的行为有着显著的差异,原始类型和引用类型:

  • 原始类型:当原始类型的数据被继承时,它们的值是通过复制的方式来传递的。每个实例都拥有这个值的自己的副本,互不影响;

  • 引用类型:当引用类型的数据被继承时,它们的值是通过地址指针的方式来传递的。也就是继承的数据是对内存中的引用,而非复制,所以修改会影响到

    所有通过这个原型继承该数据的其他实例,也就是同步更新。

代码范例二:验证上述内容

//引用类型
Parent.prototype.cars = ['Benz', 'Audi', 'BMW']; // 在自定义构造函数原型上挂载一个属性,值为引用类型的数组
function Parent(){} // 声明一个自定义构造函数

// 继承:父级构造函数的原型 赋值给 子级构造函数的原型,也就意味着子级构造函数的原型继承了父级构造函数的原型。
Child.prototype = Parent.prototype; 

function Child(){} // 声明一个自定义构造函数

var child = new Child(); // 实例化对象

child.cars[2] = 'Toyota'; // 子级构造函数实例化对象,通过属性名访问并修改子级构造函数上的原型数据

//打印结果,查看通过子级实例化对象修改原型是否会影响父级构造函数上的原型数据
console.log(Parent.prototype.cars); 
console.log(new Parent().cars); // 另一种写法,同样可以达到上一行代码的效果 
//Print Result:['Benz', 'Audi', 'Toyota']
//Explanation:由结果可以知道,在原型链继承关系中,修改引用类型的数据时,会同时影响继承该链上的所有数据内容。

//----------------------------------- 分割线 --------------------------------------------------------------

//原始类型
Parent.prototype.savings = 12000; // 在自定义构造函数原型上挂载一个属性,值为原始类型的数字
function Parent(){} // 声明一个自定义构造函数

// 继承:父级构造函数的原型 赋值给 子级构造函数的原型,也就意味着子级构造函数的原型继承了父级构造函数的原型。
Child.prototype = Parent.prototype;

function Child(){} // 声明一个自定义构造函数

var child = new Child(); // 实例化对象

child.savings++; // 子级构造函数实例化对象,通过属性名访问并修改子级构造函数上的原型数据

//打印结果,查看通过子级实例化对象修改原型是否会影响父级构造函数上的原型数据
console.log(Parent.prototype.savings); // 12000
console.log(child.savings); // 12001
console.log(child); //{savings: 12001}
//Explanation:由结果可以得知,在原型链继承关系中,修改原始类型的数据时,并不会影响继承该链上的所有数据内容,只会影响其本身,也就是发起修改的实例化对象本身。

/**
 * 
 * child.savings++; 实际上可以理解为是简化了下面这段代码:
 * child = {savings: 12000 + 1};
 * 
 */
3.2 解决办法

需求:只想继承父级构造函数上的原型中的属性或方法,然后,在子级构造函数的原型上,添加自己的原型属性或方法,但不能影响父级上的原型。

解决办法1:缓冲器隔离(姑且这么叫) 。

  • 思路:新建一个构造函数,在整个程序中,没有任何行为,纯粹就是为了承上启下的作用;
  • 实例演示:查看代码范例三。

解决办法2:简化上述操作。

  • 思路:就是把第一个解决办法,抽象化,也叫函数化;
  • 实例演示:查看代码范例四。

代码范例三:缓冲器隔离

Parent.prototype = { cars: ['Benz', 'Audi', 'BMW'] };
function Parent() {}

function Child() {}

//-------------- 缓冲器隔离 ---------------
function Buffer() {}
Buffer.prototype = Parent.prototype;
var buffer = new Buffer();
Child.prototype = buffer;
//-------------- 缓冲器区隔离 ---------------

//-------------- 缓冲器区隔离 -------------
// Parent.prototype;
// Child.prototype = Parent.prototype;
//-------------- 缓冲器区隔离 -------------


Child.prototype.age = 18;
Child.prototype.act = function() {
    if(this.age > 17){
        return '我已经成年了,可以在父亲的三辆车:' + this.cars + '。任选一辆,出去兜风了!';
    }else{
        return '未成年,不能开车';
    } 
 }
var child = new Child();
console.log(child.act());
console.log(Parent.prototype); // 观察是否有影响
console.log(buffer); // 为什么是buffer,而不是 Buffer.prototype?因为 Child 继承的是 buffer 对象,然后在从 buffer 对象的原型上获取 cars。
console.log(Child.prototype); //查看 Child 原型

打印图解: image-20240104082640085.png image-20240104082659148.png 代码范例四:函数化

//缓冲器 函数化
function inherite(Target, Original) {
    function Buffer() {}
    Buffer.prototype = Original.prototype;
    Target.prototype = new Buffer();
    //设置构造器指向
    Target.prototype.constructor = Target;
    //设置继承源指向
    Target.prototype.super_class = Original; 
}
//实际应用
Parent.prototype = { cars: ['Benz', 'Audi', 'BMW'] };
function Parent() {}
function Child() {}

inherite(Child, Parent); //函数化实际应用

Child.prototype.age = 18;
Child.prototype.act = function() {
    if(this.age > 17){
        return '我已经成年了,可以在父亲的三辆车:' + this.cars + '。任选一辆,出去兜风了!';
    }else{
        return '未成年,不能开车';
    } 
 }
var child = new Child();
console.log(child.act());
console.log(Parent.prototype);
console.log(child)
3.3 企业级写法

企业级写法中的缓冲器:立即执行函数 + 函数化缓冲器 + 闭包

先来回顾闭包的各种写法:代码范例五

企业级写法:代码范例六

代码范例五:回顾闭包

//回顾闭包1
function test(){
    var num = 0; //私有变量
    function add(){
        num++;
        console.log(a);
    }
    return add;
}
var add = test();
add(); // 1
add(); // 2
add(); // 3

//回顾闭包2
function test(){
    var num = 0; //私有变量
  	var compute = {
        add: function(){
            num++;
            console.log(num);
        },
        minus: function (){
            num--;
            console.log(num);
        }
    }
    return compute;
}
var compute = test();
compute.add(); // 1
compute.add(); // 2
compute.minus(); // 1

//回顾 构造函数闭包
function Compute(){
    var num = 0;
    this.add = function (){
        num++;
        console.log(num);
    }
    this.minus = function (){
        num--;
        console.log(num);
    }
    //return this 隐式的this
}
var compute = new test(0);
compute.add();
compute.add();
compute.add();
compute.minus();

代码范例六:立即执行 + 缓冲器函数化闭包

//缓冲器函数化闭包
function test (){
    function Buffer(){}
    function inherit(Target, Origin){
        Buffer.prototype = Origin.prototype;
        Target.prototype = new Buffer();
        Target.prototype.constructor = Target;
        Target.prototype.super_class = Origin;
    }
}

//企业级的写法 雅虎当年在yui3里封装的一个方法
//立即执行 + 缓冲器函数化闭包
var inherit = (function (){
    function Buffer(){}
    return function (Target, Origin){
        Buffer.prototype = Origin.prototype;
        Target.prototype = new Buffer();
        Target.prototype.constructor = Target;
        Target.prototype.super_class = Origin;
    }
})();
3.4 企业级写法的范例

分析代码范例七:

  • 作用域的应用:代码中的立即执行函数(IIFE)用于创建一个封闭的作用域,有效的防止全局变量命名的污染;
  • 原型链继承隔离:inherit 函数是这段代码中实现继承隔离的核心,通过创建一个临时的构造函数(Buffer),将子类(FrontEndBackEnd)的原型指向父类(Program)的原型。这种方法避免在子类中新增属性或方法,同步更新到父类。

代码范例七:企业级写法的范例

 var inherit = (function(){
  function Buffer(){}
  return function(Target, Origin){
    Buffer.prototype = Origin.prototype;
    Target.prototype = new Buffer();
    Target.prototype.constuctor = Target;
    Target.prototype.super_class = Origin;
  }
})();


var initProgram = (function (){
  function Program (){}
  Program.prototype ={
    name: '程序员',
    tool: '计算机',
    duration: '8个小时',
    say: function(){
      console.log('我是一名' + this.myName + this.name + ',我的工作是用' + this.tool + '编写应用程序,' + '我每天工作' + this.duration + ',我的工作需要用到' + this.lang + '。');
    }
  }
  function FrontEnd (){}
  function BackEnd (){}
  inherit(FrontEnd, Program);
  inherit(BackEnd, Program);
  FrontEnd.prototype.lang = ['Html', 'CSS', 'JavaScript'];
  FrontEnd.prototype.myName = '前端';
  BackEnd.prototype.lang = ['Node', 'PHP', 'Sql'];
  BackEnd.prototype.myName = '后端';

  return {f1: FrontEnd, B1: BackEnd};
})();

var frontend = new initProgram.f1();
frontend.say();
//Print Result:是一名前端程序员,我的工作是用计算机编写应用程序,我每天工作8个小时,我的工作需要用到Html,CSS,JavaScript。
3.5企业级协同开发 --- 原生开发

​ 在一个 JavaScript 文件中,由多人开发,且每人负责一个功能模块,然后汇总一起,在根据当前程序内的上下文执行环境,并执行某个或多个功能,这

就是协同开发。

Tips:

init 这种命名,一般都认为是初始化的意思。

代码范例八:协同开发

// 功能1:显示当前时间,由开发者A负责
var showCurrentTime = (function () {
  const now = new Date();
  document.getElementById('time').innerText = now.toLocaleTimeString();
})();

// 功能2:改变背景颜色,由开发者B负责
var changeBackgroundColor = (function (color) {
  document.body.style.backgroundColor = color;
})();

// 功能3:显示用户欢迎信息,由开发者C负责
var showWelcomeMessage = (function showWelcomeMessage(username) {
  document.getElementById('welcome').innerText = `Welcome, ${username}!`;
})();

//主程序,整合所有功能
function init (){
  // 假设用户名称是从某处获得的
  const username = "Alice";
  showCurrentTime();
  changeBackgroundColor('lightblue');
  showWelcomeMessage(username);
  // 每秒更新时间
  setInterval(showCurrentTime, 1000);
}

// 通过 window.onload 方法,等待所有资源加载完毕后,在执行主程序(init())。
window.onload = function() {
  init ()
};