eleven
一:原型链
1.1 探究 --- 构造函数实例化后,原型对象和实例化对象。
代码范例一:观察打印结果
function Car(){} //一个空的 Car 的自定义构造函数
var car = new Car(); //实例化对象
console.log(Car.prototype); //打印 Car 自定义构造函数的原型对象
console.log(car); //打印实例化对象
代码范例一图解:
代码范例二:
//验证 prototype 与 __proto__ 的关系
console.log(Car.prototype === car.prototype) //打印结果:false,为什么?
console.log(Object.getPrototypeOf(car) === Car.prototype); //打印结果:true,等价于第4行代码
console.log(Car.prototype === car.__proto__) //打印结果:true
console.log(Car.prototype.__proto__ === car.__proto__.__proto__) //打印结果:true
解释 上述代码:
在 JavaScript 中,每个实例化对象都有一个
__proto__属性,该属性指向其构造函数的原型对象。而构造函数本身有一个prototype属性,这个属性指向新创建的实例对象的原型。因此,
Car.prototype指向Car构造函数的原型对象,因此car.__proto__指向Car.prototype,两个完全相等 。 代码第二行中,
car.prototype这个表达式是错误的。所有对象包括实例化对象car都没有名为prototype的属性,但,Car.prototype一旦设置了属性或方法,实例化对象 car 是可以继承原型上的属性与方法,也意味着就可以使用
car.prototype.xx的方法 。 代码第三行中,
Object.getPrototypeOf(car)来获取car的原型。Object.getPrototypeOf()等价于car.__proto__,虽然__proto__在实践中使用广泛,但 ECMAScript 262 中推荐使
Object.getPrototypeOf()和Object.setPrototypeOf()来获取和设置对象的原型。总结:
Car.prototype是Car构造函数的原型对象。car.__proto__或Object.getPrototypeOf(car)是car实例的原型对象,它指向Car.prototype。
1.2 探究 --- 原型链继承
代码范例三:展示原型链与继承
Professor.prototype.tSkill = 'JAVA'; //在 Professor 构造函数的原型对象上挂载一个 tSkill 属性,值为 'JAVA'
function Professor(){} //创建一个名为 Professor 的空自定义构造函数
var professor = new Professor(); //实例化对象 professor
Teacher.prototype = professor; //把 professor 对象赋值给 teacher 原型对象,Teacher 的实例将继承 professor 的原型对象上的属性和方法。
function Teacher(){ //创建一个名为 Teacher 的空自定义构造函数
this.mSkill = 'JS/JQ'; //创建 mSkill 属性,值为 JS/JQ
}
var teacher = new Teacher(); //实例化对象 teacher
Student.prototype = teacher; //把 teacher 对象赋值给 student 原型对象, Student 的实例将继承 teacher 的原型对象上的属性和方法。
function Student(){ //创建一个名为 Student 的空自定义构造函数
this.pSkill = 'HTML/CSS' //创建 pSkill 属性,值为 HTML/CSS
}
var student = new Student(); //实例化对象 student
//此时的 student 就已经继承了 teacer 和 professor 对象的属性与方法,以及相应的原型
console.log(student); //HTML/CSS
//既然继承了上述对象的属性与方法,以及相应的原型。那么就可以自由的访问相应的属性与方法,当然,这里没有写方法。
console.log(student.mSkill); //JS/JQ
console.log(student.tSkill); //JAVA
结论
原型链:
在 JavaScript 中,每个对象(
null除外)都有一个原型对象,而原型对象也可以拥有自己的原型对象,这样就形成一个链式结构(套娃),这就是原型链。
- 对象原型:每个对象(除了
null)都有原型对象,通过__proto__属性(ES6 推荐使用Object.getPrototypeOf()方法)访问;- 原型也是对象: 原型本身也是对象,因此它也有自己的原型。这样一层层地链接下去,形成了原型链;
- 原型链的终点: 原型链的终点是
Object.prototype,Object.prototype是 JavaScript 中所有对象的最顶层的原型,它是原型链的起点和终点。所有的对象最终都会通过原型链连接到Object.prototype,形成一个链条。- 原型链的最终终点: 在原型链中,对象的原型通过
__proto__或Object.getPrototypeOf()指向其构造函数的prototype,而Object.prototype的原型是null。所以说,null是原型链的最终终点。验证这一说法,console.log(Object.prototype.__proto__),结果为null。
Object.prototype的原型为null有以下几个原因:
- 它允许我们在对象上添加新的属性和方法,而不会影响其他对象;
- 它允许我们在对象上重写原有的属性和方法;
- 它允许我们在对象上定义私有属性和方法。
原型链继承:
原型链继承是 JavaScript 中一种对象之间实现继承关系的方式。在这种模式中,一个对象可以通过继承另一个对象的属性和方法以及原型,形成一个原型链。
1.3 探究 --- 原型链继承关系上的增删改操作
问:为什么标题中没有查?
答:实际上,在原型链继承中,查询和访问是等效的操作。在代码示例三中已经验证了这一点,所以为了简洁,这里不再赘述。
问:在原型链继承关系中,操作子对象是否可以新增或修改父对象或祖先对象中的内容?
答:可以,在操作层面,是有方法来实现。
问:删除与更改的关系?
答:删除特定的属性实际上意味着移除该属性,而更改则是指修改属性的值。如果不完全删除属性名,那么所做的更改实际上是对属性值的修改。
问:那如何删除?
答:直接操作实例化对象是不可行,只有在构造函数的原型上操作才可以。
问:引用类型与原始类型在原型链继承关系中的行为区别?
答:当引用类型的值在原型链上被更改时,这种更改会在所有继承自该原型的实例中同步体现,因为它们共享同一引用。而原始类型的值在子对象中被更改
时,这种更改仅影响该子对象,不会影响父级或祖先级的原型对象。
关于增删改的结论:
总体而言,在操作层面,原型链上的增加、删除和修改操作都是可行的。然而,在实际的开发实践中,特别是当多个实例已经创建并且共享相同原型时,
强烈建议不要轻易进行这些操作。修改原型链可能会导致程序行为变得难以预测和维护。因此,在处理原型链继承关系上的增删改操作时,需要极为谨慎,以
避免引入潜在的错误和不稳定因素。具体实践请看代码范例四。
代码范例四:验证上述内容
Professor.prototype.tSkill = 'JAVA';
function Professor(){}
var professor = new Professor();
Teacher.prototype = professor;
function Teacher(){
this.mSkill = 'JS/JQ';
this.success = {alibaba: '28', tencent: '30'};
}
var teacher = new Teacher();
Student.prototype = teacher;
function Student(){
this.pSkill = 'HTML/CSS';
}
var student = new Student();
//在原型链继承关系中,可以通过 __proto__ 属性,逐层向上查询父级或祖级实例化对象内容及原型
//特别注意:要实现下面这个功能,必须是原型链继承关系,且实例化对象相互逐层向下继承才行
//console.log(student.__proto__); //打印 teacer
//console.log(student.__proto__.__proto__); //打印 professor
// ------------------------------ 按需开启或关闭注释 -------------------------------
//实验1:
//目的1:在原型链继承关系中,操作子对象是否可以新增或修改父对象或祖先对象中的内容?
//目的2:观察在子级对象中修改的同时,原型链继承关系上的实例化对象是否都会发生改变?
//目的3:演示在原型链继承关系上的删除以及影响。
//尝试操作子对象新增祖级对象中的内容
//student.__proto__.__proto__.profInfo = { name: 'Tom', age: 45}
//console.log(professor, teacher, student)
//尝试操作子对象新增父级对象中的内容
//student.__proto__.teacherInfo = { name: 'smith', age: 25}
//console.log(professor, teacher, student)
//尝试修改祖级对象,有两种方式
//一:通过 __proto__ 属性来修改
//student.__proto__.__proto__.profInfo.age = 46;
//二:通过对象的 点语法 来进行修改
//student.profInfo.age = 46;
//观察各个对象是否发生变化
//console.log(professor, teacher, student);
//在原型链继承关系中,删除原型对象中的属性,无论是引用值或原始值,且无论是实例化前后,所产生的影响都是一样
//delete Professor.prototype.tSkill;
//delete Student.prototype.success;
//console.log(professor, teacher, student);
// ------------------------------ 按需开启或关闭注释 -------------------------------
// ------------------- 实验原始类型与引用类型开始 按需开启或关闭注释 -------------------
//实验2:
//目的1:观察引用类型与原始类型在原型链继承关系中的行为(改)区别?
//修改引用值
//student.teacherInfo.age = 23;
//结果
//console.log(student);
//修改原始值
//student.students++;
//结果
//console.log(student);
// ------------------- 实验原始类型与引用类型结束 按需开启或关闭注释 -------------------
图解打印结果:
1.4 探究 --- this 指向问题
问:在 代码范例五 中,为什么打印的是 Benz ,而不是 Audi 呢?
答:这就涉及到 this 指向问题。展开如下:
1:首先,构造函数的原型对象中,挂载了 brand 属性和 intro 方法;
2:在 intro 方法内,使用的是 this ,我们的常识告诉我们, this 是谁调用就指向谁;
3:明白 this 指向的问题,我们在来看
car.intro();4:代码的第 10 行,创建了一个实例化对象 car ,然后在 11 行中,调用
intro方法;5:在调用
intro方法时,首先在 car 对象内寻找,如果有,就立即调用其方法,如果没有,则向上寻找,这个向上就是去原型对象内查找;6:在原型对象内查找到有一个 intro 方法,则立即调用,发现
intro的方法内还有一个this.brand的指向;7:因为已经实例化对象,且对象内有
brand这个属性,因此this.brand就指向实例化对象内的brand,所以打印的是 Benz 而不是 Audi。
代码范例五:
function Car(){
this.brand = 'Benz';
}
Car.prototype = {
brand: 'Audi',
intro: function(){
console.log('我是' + this.brand + '车');
}
}
var car = new Car();
car.intro(); // 我是 Benz 车
//如果想打印 Audi ,有两种方法。
//1:修改 intro 方法内的 this 指向
//intro: function(){ console.log('我是' + Car.prototype.brand + '车')};
//2:直接使用构造函数调用,而不是修改 intro 方法内的 this 指向
//Car.prototype.intro();
1.5 探究 --- 普通函数与构造函数的返回
普通函数 ---> 如果没有写返回值,则默认是返回 undefined ;
构造函数 ---> 如果没有写返回值,则默认是返回 this ;
- 构造函数内,使用
return关键字(指令) 返回 原始值,在实例化后,还是默认返回 this;- 构造函数内,使用
return关键字(指令) 返回 引用值,在实例化后,则返回的是 引用值;
代码范例六:
//普通函数,没有写返回值 return
function normalFunction(){
var x = 0;
};
// 调用普通函数
var result1 = normalFunction();
//构造函数
function ConstructorFunction() {
this.value = 'Some value'; // 设置实例的属性
//没有返回语句,所以默认返回 this
}
// 使用 new 关键字构造函数
var result2 = new ConstructorFunction();
//打印普通函数和构造函数的结果
console.log(result1, result2);
//Print Result:undefined ►ConstructorFunction {value: 'Some value'}
//Expalanation:
//1:normalFunction 是一个普通函数,它内部没有 return 语句。当调用这个函数时,它会默认返回 undefined;
//2:ConstructorFunction 没有 return 语句。所以当使用 new ConstructorFunction() 创建一个新实例时,它会默认返回 this,即新创建的对象实例。
二:对象继承
2.1 创建对象的一般常识1
对象字面量:
var obj1 = {};系统内置的构造函数:
var obj2 = new Object(); 一般情况下,在大多数公司内前端开发中,不推荐或有明文规定不允许使用 系统内置的构造函数 创建对象。原因是,两者无任何区别,但后者跟前者相
对比时,前者可读性、易用性、简洁性,都是后者无法可比拟的。所以,一般都使用对象字面量,来直接创建对象。
2.2 创建对象的一般常识2
通过打印图解,可以看出,前两个对象的原型下面的构造器指向的是系统构造函数,即
constructor: ƒ Object()。而自定义构造函数,实例化对象后,其构造器是指向自定义构造函数
constructor: ƒ Obj()。
代码范例七:
//对象字面量
var obj1 = {};
console.log(obj1);
//系统内置的构造函数 Object()
var obj2 = new Object();
console.log(obj2);
//自定义构造函数
function Obj3(){}
var obj3 = new Obj3();
console.log(obj3);
打印图解:
2.3 探究 --- Object.create() 初识1
基本参数:
Object.create()方法,一共有两个参数,第一个参数必选,第二个参数为可选。第一个参数:可以是 对象 或 null ;
第二个参数:目前来说,还没有到接触的时候,暂且不表。
它的功能:
以一个现有对象作为原型,创建一个新对象。
- 进一步解读:
创建一个新对象,并将这个新对象的原型设置为另一个已存在的对象。这意味着新创建的对象将继承现有对象的属性和方法。
- 在进一步解读:
Object.create()的主要作用是实现对象的原型继承,这与使用构造函数创建对象,并通过原型链继承属性和方法有着相同的效果。但
Object.create()提供了一种更加直观且字面的方式来设置对象的原型。
代码范例八:验证上述内容
//常规MDN例子 --- 进行了一定的改写
var person = { //使用对象字面量声明一个对象
isHuman: false,
printIntroduction: function(){
//一个新的技巧:
//当一个对象作为即将被设置成另一个对象的原型时,在一个方法内,是可以提前使用 this 关键字指向一个未被在当前的对象中定义的属性名;
//当新对象被实例化后,其作为原型的对象中的 this.name 属性就指向了当前的新对象,意味着可以进行赋值操作,在后续使用中,就可以被正常的调用。
console.log('My name is' + ' ' + this.name + '.' + ' ' + 'Am I human?' + this.isHuman);
}
};
var me = Object.create(person); //使用 Object.create() 方法,创建一个对象并设置对象的原型,也意味这个新对象继承了 person 对象的属性与方法。
me.name = 'Smith'; //创建 me 对象的属性并赋值
me.isHuman = true; //继承的属性是可以重写
me.printIntroduction();
//Print Result:My name is Smith. Am I human?true
//原型,同样也是一个对象,作为参数传入
function Obj(){}
Obj.prototype.name = 'test';
//传参,构造函数的 prototype 也是一个对象,当然也可以作为一个参数传入进去
var newObj1 = Object.cerate(Obj.prototype);
var newObj2 = new Obj();
console.log(newObj1);
console.log(newObj2);
//上述打印的两个结果都一样,只是两个不同的对象而已
2.4 探究 --- Object.create() 初识2
问:为什么在控制台中查看对象,发现里面什么也没有,这是为什么?
答:因为,使用
Object.create(null)创建一个对象时,它不继承自Object.prototype。这意味着该对象不仅没有标准的对象方法(如toString或
hasOwnProperty),而且它实际上也没有原型链(即没有__proto__属性或内部[[Prototype]]链接)。
代码范例九:一无所有的空对象
//创建 obj1 空对象
var obj1 = Object.create(null);
console.log(obj1);
打印图解:
代码范例十:改造空对象
//创建 obj1 空对象
var obj1 = Object.create(null);
console.log(obj1);
//增加属性并赋值
obj1.num = 1;
//实例化一个对象,并指定另一个对象作为,实例化对象的原型
//继承关系
var obj2 = Object.create(obj1);
console.log(obj2);
打印图解:
代码范例十一:自造的 __proto__ 与 系统的 __proto__ 的区别
//创建 obj 空对象
var obj = Object.create(null);
//obj 对象添加一个属性与值
obj.num = 1;
//使用对象字面量声明一个对象,并创建一个属性以及赋值
var obj1 = {count: 2};
//尝试手动给 obj 对象创建一个 __proto__ 属性,并在其中挂载一个 obj1 对象
obj.__proto__ = obj2;
//进行测试
//如果可以访问到 count 的值,则能证明,可以通过手动设置 __proto__ 的方式,来指定一个原型
console.log(obj1.count); //undefined
//打印结果,证明这个方法不可行
//也就说明,Object.create(null) 创建的对象,不支持手动设置 __proto__ 属性,且也没有 __proto__ 属性。
//同时也说明,__proto__必须是系统内置的。
//可以更改系统内置的 __proto__ 的前提是对象必须有 __proto__ 属性。但是我们不能无中生有。
2.5 跑不掉的 undefined 与 null
问:undefined 与 null 是否有 toString() 方法,为什么?
答:个人理解有点长,展开如下:
- 在 JavaScript 中,undefined 和 null 是属于原始类型,并不是引用类型,也就不是对象,如果是对象就会继承 Object.prototype 中的 toString() 方法;
- 同时,原始类型中除了 undefined 和 null 之外,其它的都可以通过包装类,临时转换为对象,也就有了 toString() 方法;
- 需要了解的一个知识,包装类中的 toString() 方法,并不是继承了 Object.prototype 中的 toString() 方法,而是重新改写了这个方法。
代码范例十二:验证上述内容的一小部分
console.log(undefined.toString());
console.log(null.toString());
//Print Result: Uncaught TypeError: Cannot read properties of undefined (reading 'toString')
//Translate to Chinese:无法读取未定义的属性(读取 "toString")
2.6 验证 Object.create(null) 中是否有 toString() 方法?
通过下方的代码范例,可以很明确的看到,是没有的。
问:为什么没有?
答:因为 Object.create(null) 实例化对象时,并没有继承 Object.prototype 中定义的任何方法。
Tips:
document.write() 方法中,接收一个字符串参数,并将这个字符串作为 HTML 标签或文本写入到 HTML 文档中。所以,任何类型的数据作为参数传入进
去时,都会隐式的转换为字符串,如果不能转换,则会报错。
代码范例十三:
var num = 1;
var obj = {};
var obj2 = Object.create(null);
//document.write 隐式转换为 字符串
document.write(num);// 1
document.write(obj);// [object Object]
document.write(obj2);// Cannot convert object to primitive value
//Translate to Chinese:不能将对象转换为原始值
//手动写一个 toString()
//可以这么写,但是无意义
obj2.toString = function(){
return 'hello world!';
}
//验证下,但需要把前面的 obj2 给注释掉才行
document.write(obj2.toString()); //hello world!
2.7 认识包装类中的 toString() 方法的重写
在 JavaScript 中,数据类型的 toString() 方法,并不是直接继承 Object.prototype 中的 toString() 方法,而是重写。
代码范例十四:验证上述内容
//第一步
//观察下面的打印结果
console.log(Object.prototype);
console.log(Number.prototype);
console.log(String.prototype);
console.log(Boolean.prototype);
console.log(Array.prototype);
//Print Result:每个类型的原型中都包含一个 toString() 方法
//第二步
//观察控制台中的每一行打印结果
Object.prototype.toString.call(1);
Object.prototype.toString.call('a');
Object.prototype.toString.call(true);
//Print Result:结果以字符串的形式显示,'[object Number]' '[object String]' '[object Boolean]'
//Explanation:对象的数字类型构造函数 对象的字符串类型构造函数 对象的布尔类型构造函数
//第三步
Number.prototype.toString.call(1);
String.prototype.toString.call('a');
Boolean.prototype.toString.call(true);
Array.prototype.toString.call([1, 2, 3]);
//Print Result:结果以字符串的形式显示,'1' 'a' 'true' '1,2,3'
//Explanation:通过上述三步,可以得知:
//1、在对象的原型顶端原型中 toString() 方法,显示的结果是以字符串的形式显示 '[object Type]' 其中 Type 会被视为相应的数据类型;
//2、在对应的数据类型中的 toString() 方法,显示的结果是以字符串的形式展示数据内容,而不是数据类型;
//3、在 JavaScript 中,数据类型的 toString() 方法,并不是直接继承 Object.prototype 中的 toString() 方法,而是重写。
三:call 与 apply 方法的初步认识
基本概念:
在 JavaScript 中,
call,apply, 和bind都是Function.prototype上的方法,用于控制函数的调用过程。它们允许你设置函数执行时的this上下文(即函数内部的
this值)以及传递参数。尽管它们的功能相似,但它们在如何接收参数和具体用法上有所不同。
3.1 call
Function实例的call()方法会以给定的this值和逐个提供的参数调用该函数。语法:
call(thisArg)call(thisArg, arg1, /* …, */ argN)参数:
thisArg--- 必选,在调用func时要使用的this值。
- 个人目前的理解,这个参数,是一个对象,或者是 this 指向,当然是需要根据当前代码执行期的上下文理解。
arg, ..., argN--- 可选,参数。
代码范例十五:验证上述内容
//call简单示例一
function Greet() { //声明了一个自定义构造函数
//等待 this 指向
console.log(this.animal, "的睡眠时间一般在", this.sleepDuration, "之间");
}
var obj = { //对象字面量
animal: "猫",
sleepDuration: "12 到 16 小时",
};
Greet.call(obj); //利用 call 方法动态更改函数执行期上下文 this 指向
//Print Result:猫 的睡眠时间一般在 12 到 16 小时 之间
//Explanation: 执行时的个人理解
//在执行 Greet.call(obj) 时,Greet 函数的上下文 this 指向被显式的设置为 obj 对象。因此,Greet 函数内的 this.animal 和 this.sleepDuration 分别指向了 obj 对象的 animal 和 sleepDuration 属性名与值。因此打印内容为预期结果。
//call简单示例二
function Car(brand, color) { //声明了一个自定义构造函数
this.brand = brand; //初始化实例化对象的属性名与值
this.color = color; //同上
}
var Car1 = {}; //使用对象字面量的方式 声明一个空对象
var Car2 = {}; //同上
Car.call(Car1, 'Benz', 'red'); //利用 call 方法动态更改函数执行期上下文 this 指向
Car.call(Car2, 'Audi', 'black'); //同上
console.log(Car1, Car2);
//Print Result:{ brand: 'Benz', color: 'red' } { brand: 'Audi', color: 'black' }
//Explanation:与 call 简单示例一不同之处就传参。因此打印内容为预期结果。
3.2 apply
Function实例的apply()方法会以给定的this值和作为数组(或类数组对象)提供的arguments调用该函数。语法:
apply(thisArg)apply(thisArg, argsArray)参数:
thisArg--- 必选,在调用func时要使用的this值。
- 个人目前的理解,这个参数,是一个对象,或者是 this 指向,当然是需要根据当前代码执行期的上下文理解。
apply(thisArg, argsArray)--- 可选,传入一个数组。
代码范例十六:验证上述内容
//apply 简单示例
function Car(brand, color) { //声明了一个自定义构造函数
this.brand = brand; //初始化实例化对象的属性名与值
this.color = color; //同上
}
var Car1 = {}; //使用对象字面量的方式 声明一个空对象
var Car2 = {}; //同上
Car.apply(Car1, ['Benz', 'red']); //利用 apply 方法动态更改函数执行期上下文 this 指向
Car.apply(Car2, ['Audi', 'black']); //同上
console.log(Car1, Car2);
//Print Result:{ brand: 'Benz', color: 'red' } { brand: 'Audi', color: 'black' }
//Explanation:与 call 唯一的区别,就是传参是数组而已,目前的认知。因此打印内容为预期结果。
3.3 this 的指向改变,应用场景初步认识
call()和apply(),这两个动态的改变函数内的 this 指向,在使用上一般常用的是 apply() 方法。为什么要用 动态的改变函数内的 this 指向 功能?
扩展方法,借用其他构造函数的方法和属性;
常规的企业开发中,多人协同写一个大的功能,进行分组开发,最终汇总调用;
不同的类型分开写,然后再汇总。
代码范例十七:验证上述内容
// 加减乘除功能演示
function Count(){ // Count 自定义构造函数定义了两个基本的数学运算方法:加法和减法。
// 加法运算:返回两个数的和。
this.plus = function(a, b){
return a + b;
}
// 减法运算:返回两个数的差。
this.minus = function(a, b){
return a - b;
}
}
function FullCount(){ // FullCount 自定义构造函数继承 Count 并添加了乘法和除法操作。
// 使用 apply() 方法,继承 Count 自定义构造函数的方法,使得 FullCount 实例也具有 plus 和 minus 方法。
Count.apply(this);
// 乘法运算:返回两个数的乘积。
this.multiplied = function(a, b){
return a * b;
}
// 除法运算:返回两个数的商。
this.div = function(a, b){
return a / b;
}
}
var fullCount = new FullCount(); // 创建 FullCount 的实例。
console.log(fullCount.plus(2, 3)); //输出: 5
console.log(fullCount.minus(5, 3)); // 输出: 2
console.log(fullCount.multiplied(9, 3)); // 输出: 27
console.log(fullCount.div(7, 3)); // 输出: 2.3333333333333335