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 的结构
代码范例一:图解
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中进行原型对象的重新指向,所以结果不一样。
//以下为图解以及原型图解
二: 插件的基本认知
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