JavaScript学习笔记十

90 阅读15分钟

ten

一:什么是原型?

英文:prototype,翻译成中文就是,原型。

中文:原型 --- > 什么是原型,按照字面理解,就是原始的样板或模型

在 JavaScript 中,是什么?它起到什么作用?它有什么应用场景?

1.1 探究 --- prototype(原型)是一个什么东西?

​ 在 JavaScript 中,数据类型分为,原始类型与引用类型。其引用类型中就包含了函数 --- function(){} 。同时,在 JavaScript 当中,说一切皆为对象。所以,下面代码范例一showPrototype.prototype 使用了点语法,就可以认为 prototype(原型)是 function(Object) 对象的一个属性。那么在控制台中尝试打印下,看看结果是什么?发现也是一个对象,至于里面的东西,暂且先不做深究,至少现在可以看出 prototype 一个大概的样子。

总结

1:prototype 其实是 function 对象的一个属性 ;

2:同时 prototype(原型)它也是一个对象。

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

function showPrototype(){}
console.log(showPrototype.prototype);//打印 prototype 的结构

代码范例一:图解

image-20231217121925170.png

1.2 探究 --- prototype(原型)是否可以添加属性与方法?

​ 在 JavaScript 中,每个对象都有一个 prototype 属性,它指向一个对象。这个对象称为原型对象。原型对象可以包含属性和方法。因此,所有被构造函

数实例化出来的对象都可以继承 prototype(原型)上的属性和方法。

总结

1:由此可以得出,所有被构造函数构造出来的对象都可以继承prototype(原型)上的属性和方法;

2:prototype(原型)对象是构造函数实例化后每个对象的公共父级。

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

function Handphone(color, brand){ //自定义构造函数
	this.color = color; //形参赋值
	this.brand = brand; //同上
	this.screen = '18:9'; //固定值
	this.system = 'Android'; //同上
}
var hp1 = new Handphone('red', '小米'); //实例化并传实参,同时赋值给 hp1 ,使之为对象;
var hp2 = new Handphone('black', '华为'); //同上对象名有所不同

//根据 1.1 小章节中所述,prototype 是 function 对象的属性,它自身也是一个对象,那么根据这一特点,尝试来写一个值和方法看看。
Handphone.prototype.rom = '64G'; 
Handphone.prototype.ram = '6G';
Handphone.prototype.call = function(){
    console.log('hello world!');
}
//尝试打印看看,是一个什么结果
console.log(hp1.rom); //Print Result:64G
console.log(hp2.ram); //Print Result:6G
hp2.call(); //Print Result:hello world!
//由此可以得出,所有被构造函数构造出来的对象都可以继承prototype(原型)上的属性和方法。
//prototype(原型)对象是构造函数实例化后每个对象的公共父级。
1.3 探究 --- prototype(原型)中的属性和方法,在访问或调用时的优先级。

访问对象属性的值,如果对象中有就调用其属性中的值,如没有,则去父级(prototype)找,同时如果父级中也没有,则返回 undefined。

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

//实验一
function DisplayTesting(){} //空自定义构造函数
DisplayTesting.prototype.name = 'proto'; //在 prototype 对象上新建一个属性并赋值
var dispaly = new DisplayTesting(); //实例化对象
cosole.log(dispaly.name) //尝试直接使用继承 prototype 上的属性名来打印
//Print Result:proto
//Explanation:可以看出,是可以直接使用对象父级的属性名来获取值的,但是注意自定义构造函数在当前环境下为空。

//实验二
function DisplayTesting(){ //自定义构造函数
    this.name = 'protos'; //固定值
} 
DisplayTesting.prototype.name = 'proto';//在 prototype 对象上新建一个同名的属性并赋值
var dispaly = new DisplayTesting(); //实例化对象
cosole.log(dispaly.name) //尝试使用属性名来获取值,看看打印出来的是哪一个值?
//Print Result:protos
//Explanation:由结果可以得知,在 实验一 与 实验二 的环境中,访问对象属性的值,如果对象中有就调用其属性中的值,如没有,则去父级(prototype)找,同时如果父级中也没有,则返回 undefined。
1.4 探究 --- prototype(原型)的作用是什么?

在 JavaScript 中,prototype(原型)的作用可以归纳为三点,以下为展开具体说明:

1:节省内存空间,当创建一个构造函数与定义属性与方法时,其值与内容都是定值,且不被传参所影响。当在实例化多个对象时,每个对象都会具有相同的属性与方法,也就造成内存空间的浪费。解决办法就是把相同的属性与方法定义在 prototype 对象内,实现每个实例化对象从 prototype 对象内继承这些相同的属性与方法。而每个实例化对象只需存储(定义)自己独有的属性与方法,这就可以实现节省内存空间的作用;

2:实现数据共享(继承),因为 prototype 对象是构造函数实例化后每个对象的公共父级。就可以认为,通过构造函数实例化的对象,都可以通过这一特

性,实现属性与方法的复用,这也就是实现了数据共享(继承)的作用;

3:prototype 的实际用途,常常在实际开发 JavaScript 插件时,也是利用 prototype 对象的数据共享(继承)这一特点。在构造函数内只定义一些需要传参的属性,作为插件的配置项。而相同的方法和具有定值的属性则写在 prototype 对象内,作为插件的功能。

静态

1.5 探究 --- prototype(原型),尝试在实例化构造函数的对象上对 prototype 对象属性的增删改查?

答案是否定的,只是作为尝试性质的实验。

通过下面的代码验证,可以得知,想要通过实例化构造函数的对象,来操作原型,是不可行的。原因就是,原型是实例化构造函数对象的公共父级。

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

//实验基底代码
function DisplayTest(){} //创建一个空自定义构造函数
DisplayTest.prototype.name = 'test'; // 在 prototype 对象上添加属性
var showAdd = new DisplayTest(); //使用 new 关键字实例化构造函数,生成一个对象
console.log(DisplayTest.prototype, showAdd); //打印两个结果对比
//Print Result:{name: 'test', constructor: ƒ}, DispalyTest {}
//Explanation:逗号前的是 prototype 对象结果,逗号后是实例化对象后结果

//实验 --- 增
//尝试,在实例化构造函数的对象上增加一个原型属性是否可行?
showAdd.name = 'testing'; 
console.log(DisplayTest.prototype)
//Print Result:{name: 'test', constructor: ƒ}
//Explanation:并没有任何变化

//实验 --- 删
//尝试,在实例化构造函数的对象上删除一个原型属性是否可行?
delete showAdd.name;
console.log(DisplayTest.prototype)
//Print Result:{name: 'test', constructor: ƒ}
//Explanation:并没有任何变化

//实验 --- 改 实验 --- 查
//尝试,在实例化构造函数的对象上修改一个原型属性是否可行?
//尝试,在实例化构造函数的对象上查一个原型属性是否可行?

//Explanation:通过增删的实验,就可得知,实验不可行。至于改和查,实际是就是重新赋值和访问。都一样,也就不需要在浪费时间做测试了。
1.6 探究 --- prototype(原型)的书写范例。

知道 prototype 是一个对象,那么应该就可以使用对象的格式来书写代码。

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

//改造前代码
function HandPhone(color, brand){
    this.color = color;
    this.brand = brand;
    this.screen = '18:9';
    this.system = 'android';
}
HandPhone.prototype.rom = '64G';
HandPhone.prototype.ram = '6G';
HandPhone.prototype.call = function(){
    console.log('hell world');
}
var h1 = new HandPhone('red', '小米');
var h2 = new HandPhone('black', '谷歌');

console.log(h1.rom, h1.ram)//64G, 6G
console.log(h2.rom, h2.ram)//64G, 6G
h1.call();//hello world

//改造后代码
function HandPhone(color, brand){
    this.color = color;
    this.brand = brand;
    this.screen = '18:9';
    this.system = 'android';
}

HandPhone.prototype = {
    rom: '64G',
    ram: '6G',
    call: function(){
    console.log('hell world');
	}
}
var h1 = new HandPhone('red', '小米');
var h2 = new HandPhone('black', '谷歌');

console.log(h1.rom, h1.ram)//64G, 6G
console.log(h2.rom, h2.ram)//64G, 6G
h1.call();//hello world
1.7 探究 --- prototype(原型)中 constructor 初步认识。

constructor 属性 --- 指向构造函数本身,可修改指向。

Tips1:在控制台中显示,{constructor: ƒ},表示该对象的 constructor 属性的值是一个函数, ƒ 表示函数,这是一种缩略形式。

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

//观察 自定义构造函数的原型对象

function HandPhone(){} //创建一个空的自定义构造函数
console.log(HandPhone.prototype); //打印自定义构造函数的原型对象
//Print Result:The results are as follows(结果如下)
//▼{constructor: ƒ}	自定义构造函数原型对象上的一个属性
//  ►constructor: ƒ HandPhone()	默认指向当前自定义构造函数本身
//  ►[[Prototype]]: Object


//修改指向
function HandPhone(){} //创建一个名为 HandPhone 空的自定义构造函数
function TelePhone(){} //创建一个名为 TelePhone空的自定义构造函数
HandPhone.prototype = { //修改 HandPhone 自定义构造函数默认指向
    constructor: TelePhone
}
console.log(HandPhone.prototype);
//Print Result:
//▼{constructor: ƒ}	自定义构造函数原型对象上的一个属性
//  ►constructor: ƒ TelePhonee() 这个是修改指向,这里指向了 TelePhone 自定义构造函数
//  ►[[Prototype]]: Object
1.8 探究 --- ES5 中的 __proto__ 是什么?

问:__proto__ 是什么?

答:首先它是一个对象内部的属性,每个引用类型都有这个内部属性,这里就不得不说,在 JavaScript 中,一切皆为对象。在构造函数中,必须是实例化对象后,才会生成 __proto__ ,它不隶属于构造函数,仅仅只是挂靠在构造函数中,一旦实例化后,它就指向实例化对象的原型。可以更加直观的认为,它就是一个容器,存放 prototype 对象。

问:为什么 __proto__ 要书写成左右两边双下划线(__)?

答:系统内置属性的写法,没有其它原因。

问:是否可以修改 __proto__ 的值?

答:当然可以修改。

Tips1:

现在是2023年12月19日,在 chrome (Edge、Firefox)浏览器的控制台中, __proto__ 修改成 [[prototype]],只是修改了显示样式而已,功能性都一样。

Tips2:

在 Chrome 等浏览器的控制台输出中,[[prototype]] 的双方括号表示这是一个内部属性,而不是 JavaScript 语言规范中明确使用的语法。它用于表示对象的原型链。

  • [[prototype]] 的第一个括号: 表示这是一个内部属性,不是直接由 JavaScript 代码访问的属性。
  • [[prototype]] 的第二个括号: 表示这是一个对象的原型链。通过这个属性,对象可以访问到其原型链上的属性和方法。

这种表示方式是为了在控制台中更清晰地标识这是一个内部属性,避免与 JavaScript 代码中可能使用的其他语法冲突。

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

//展示 __proto__
function Car(){} //创建一个名为 Car 的空自定义构造函数
Car.prototype.name = 'Benz'; //在原型对象上创建一个名为 name 的属性并赋值为 Benz
var car = new Car(); //实例化对象
console.log(car); //打印实例化对象
//Print Result:
//ES5 的显示结果
//▼Car {}
//  ▼__proto__: Object //实例化后才有的结果,所以它属于实例化对象
//  name:"Benz"	   //__Proto__ 中存储了实例化对象的原型,也意味着 __Proto__ 就是一个容器,包含了 prototype 对象所有内容 。
//  ►constructor:ƒ Car()
//  ►__proto__: Object
//ES6 的显示结果
//▼Car {}
//  ▼[Prototype]]: Object
//   name:"Benz"
//   ►constructor:ƒ Car()
//   ►[Prototype]]: Object

//修改 __proto__ 
function Car(){} //创建一个名为 Car 的空自定义构造函数
Car.prototype.name = 'Benz'; //在原型对象上创建一个名为 name 的属性并赋值为 Benz
var car = new Car(); //实例化对象
var newCar = {	//创建一个对象
    name:'Audi'
}
console.log(car.name); //Benz
car.__proto__ = newCar; //修改 __proto__ 属性
console.log(car.name) //Audi
1.8 探究 --- prototype(原型)实例化前后的变化。

由下面的代码范例结果可以得知,实例化前后的原型对象指向,会对实例化对象有着不同的影响,其结果也会有所不同的变化。

1:Car.prototype.name = 'Benz'; 这是原型对象中某个属性的值进行修改,并不是修改指向;

2:Car.prototype ={name: 'Audi'}; 这是对原型对象重新指向的操作;

如 2 发生在实例化前的最后一步操作,则这是实例化每个对象的公共父级;

如 2 发生在实例化后,则需再次实例化后,才是实例化每个对象的公共父级。

代码范例八:

//现象1
function Car(){} //创建名为 Car 的自定义构造函数
Car.prototype.name = 'Benz'; //在原型对象上,新增一个属性,并赋值
var car = new Car(); //实例化对象
Car.prototype.name = 'Audi'; //给原型对象中的一个属性进行重新赋值
console.log(car.name); //Audi 打印实例化对象中的属性,实例化对象中没有,则去公共父级上查找,并打印出来

//现象2
Car.prototype.name = 'Audi'; //在原型对象上,新增一个属性,并赋值
function Car(){} //创建名为 Car 的自定义构造函数
var car = new Car(); //实例化对象
Car.prototype.name = 'Benz'; //给原型对象中的一个属性进行重新赋值
console.log(car.name); //Audi 打印实例化对象中的属性,实例化对象中没有,则去公共父级上查找,并打印出来
//Explanation:
//根据现象1与现象2的结果,在给原型对象新建一个属性,并直接赋值的操作后,发现,无论是书写在实例化对象的前后,都是按照书写顺序来显示其值,并没有什么变化。

//现象3
Car.prototype.name = 'Benz'; //在原型对象 Car.prototype 上添加 name 属性,并赋值为 Benz
function Car(){} //声明了一个 Car 自定义构造函数
var car = new Car(); //创建 Car 的实例化对象 car
Car.prototype ={ //重新设置 Car 的原型对象
    name: 'Audi' //重新设置原型对象的 name 属性为 Audi
}
console.log(car.name); //输出实例化对象 car 的 name 属性值为 Benz
//Question1:为什么打印的结果,会是 Benz,而不是 Audi 呢?
//Explanation1:
//1:因为在实例化对象之前,就在原型对象上创建了一个 name 属性值为 Benz;
//2:接着实例化对象时,car 对象内部 __proto__(ES6 是[[prototype]])的属性(对象)就默认的指向 Car.prototype,就形成一条完整的链式关系;
//3:然后,Car.prototype 又被重新指向为一个新对象 {name: 'Audi'},此时,Car.prototype 不在指向原来的原型对象,而是指向这个新的原型对象;
//4:需在控制台中打印 car.name,则先去自身对象内查找,没有,则向上也就是原型对象中查找,有,就打印 name 为 Benz;
//5:此时,car 的原型对象,在实例化之前,就已经被明确指向(第2步),尽管,在第 3 步时又被重新指向原型对象,但这是发生在实例化之后,所以,结果为 Benz。

//Question2:明明都是重新赋值,为什么打印的结果不跟 现象1与现象2 的结果一样呢?
//Explanation2:因为现象1与现象2是对原型对象中的属性重新赋值(修改),而不是像现象3中进行原型对象的重新指向,所以结果不一样。
//以下为图解以及原型图解

image-20231220215135311.png

image-20231220211454954.png

二: 插件的基本认知

2.1 return 的作用

在 JavaScript 中,return 是一条指令(关键字),用于从函数中返回一个值到外部环境(可以是全局,也可以是上一步环境中)中去。

代码范例一:

//闭包
function test(){
    var a = 1;
    function plus(){
        a++;
        console.log(a);
    }
    return plus; //返回一个函数到全局
}
var plus = test();
plus(); //2
plus(); //3
plus(); //4
2.2 window 全局对象(浏览器环境)

在浏览器中,window 是全局对象,它包含了浏览器环境的各种全局属性和方法。

代码范例二:

//window 全局对象的应用
function test(){
    var a = 1;
    function plus1(){
        a++;
        console.log(a);
    }
    window.plus = plus1; //利用 window 全局对象的作用,把 plus1 函数 赋值给 全局对象 plus,就跟 return 一样;
}
test();//函数执行时,就已经把 plus1 函数 赋值给 全局对象,此时就可以在外部直接用全局变量 plus,来调用执行 plus1 函数;
plus(); //2
plus(); //3
plus(); //4
2.3 立即执行函数 之 window。

回顾立即执行函数,并结合 window 全局对象。

代码范例三:

//典型的立即执行函数
var add = (function test(){
    var a = 1;
    function plus(){
        a++;
        console.log(a);
    }
    return plus;
})();
add(); //2
add(); //3
add(); //4

//立即执行函数结合 window
(function test(){
    var a = 1;
    function plus(){
        a++;
        console.log(a);
    }
    window.plus= plus; //通过全局变量,将 plus 函数返回到全局环境中,相当于 return 省去了在全局中的赋值操作。
})();
plus(); //2
plus(); //3
plus(); //4
2.4 插件

分号的使用:多个功能性独立的插件,合并在一个 JS 文件中时,需使用分号 ; ,在代码的头部进行分割,这是一种约定俗成的写法,具体看代码范例四;

插件的常规书写方式:立即执行函数包装 + 插件主体 + window全局对象返回(将插件暴露给全局),具体看代码范例五;

为什么要用立即执行函数去写插件:用立即执行函数去隔离,防止变量污染;

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

//分号的使用
//以下为伪代码
;(function(){
    function plugIn1(){}
})()
;(function(){
    function plugIn2(){}
})()
;(function(){
    function plugIn3(){}
})()
//Question:为什么要添加分号?
//Explanation:约定俗成的写法,主要原因,怕是书写过多代码时,忘记在多个代码块之后添加分号,所以,直接在开头就添加,这样,就不会遗漏。

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

//演示一个具有加减乘除功能插件的案例
//接收两个参数,使用方法进行加减乘除的功能

//第一步:写插件,保存为 count.js
(function (global) { //该函数接受一个参数 global,它在浏览器环境中通常是 window 对象。这样可以确保插件在不同的环境中都能正确运行。
  function Count(opt) {
    this.op1 = opt.op1;
    this.op2 = opt.op2;
  }
  Count.prototype = {
    plus: function () {
      return this.op1 + this.op2;
    },
    minus: function () {
      return this.op1 - this.op2;
    },
    multiplied: function () {
      return this.op1 * this.op2;
    },
    div: function () {
      return this.op1 / this.op2;
    }
  };
  global.Count = Count; // 将插件暴露给全局对象
})(window);

//第二步:在HTML文件中引入 count.js 文件
<script src="count.js"></script>

//第三步:在HTML文件的 <script></script> 中创建插件实例,以及调用插件方法
var myCount = new Count({op1: 10, op2: 5}); // 创建插件实例
// 使用插件方法
console.log(myCount.plus()); //15
console.log(myCount.minus()); //5
console.log(myCount.multiplied()); //50
console.log(myCount.div()); //2

代码范例六:

//学习插件的简单写法
//实现:接收两个参数,使用方法进行加减乘除的功能
(function () {
    function Count(opt) {
        this.op1 = opt.op1;
        this.op2 = opt.op2;
    }
    Count.prototype = {
        plus: function () {
            return this.op1 + this.op2;
        },
        minus: function () {
            return this.op1 - this.op2;
        },
        multiplied: function () {
            return this.op1 * this.op2;
        },
        div: function () {
            return this.op1 / this.op2;
        }
    };
    window.Count = Count;
})();

//创建实例
var myCount = new Count({
    op1: 12,
    op2: 3
});
console.log(myCount.plus()); //15
console.log(myCount.minus()); //9
console.log(myCount.multiplied()); //36
console.log(myCount.div()); //4