批改状态:合格
老师批语:
这个博文是我测试最痛苦的,它涉及太底层了,老师给我打开了prototype的大门,但如何深入理解则查阅了大量的网文,尤其是原型链和继承的探索,让我不断反复推翻自己的结论,现在博文也不算完美,不过是目前我最高的理解了,以后更深入再说吧。
JS中万物皆是对象,应该是JS中最经典的口头禅了,但是在我测试Object和Function等内置对象时,则出现深深的疑惑,先看测试结果
console.dir(Object);console.log('Object => ', Object.prototype.toString.call(Object));console.dir(Function);console.log('Function => ', Object.prototype.toString.call(Function));console.dir(Number);console.log('Number => ', Object.prototype.toString.call(Number));

网上普遍解释: JS中对象概念应该分为两种:一是普通对象,另一个是函数对象。凡是通过new Function()创建的对象都是函数对象,其他的都是普通对象。function(){}实质也是调用new Function()来创建的。
至少是我理解JS的万物皆是对象是指数据类型的对象,其实不然,还是先看下面测试吧
function Person(name){this.name=name;this.hello=function(){console.log(`Hello ${this.name}`);};}console.log(Object.prototype.toString.call(123));console.log(Object.prototype.toString.call('abc'));console.log(Object.prototype.toString.call(true));console.log(Object.prototype.toString.call(undefined));console.log(Object.prototype.toString.call(null));console.log(Object.prototype.toString.call([]));console.log(Object.prototype.toString.call(Person));console.log(Object.prototype.toString.call({}));

相信从上图中应该能发现点什么,也是图中我以前没注意,而在测试时才注意到的细节,就是通过Object.prototype.toString.call显示数据类型时,总是由两部分组成:小写object和大写开头的数据类型。
所谓JS万物皆是对象,依据就是前面的object,它指明数据特性,就是说数据都是对应的Number、String、Boolean…Function和Object等函数的实例。而数据类型中对象则是由Object()创建的实例。
普通对象和函数对象: 函数对象就是指object Function,其它则是普通对象,前者拥有proto和prototype两个重要属性,后者只拥有proto属性,数值、字符串等都拥有proto属性喔,现在应该更理解JS万物皆是对象的说法了。

上面已经说到普通对象有proto属性,而函数对象还具有prototype属性,constructor是构造函数,是二者属性下的重要属性,也是理解原型链的关键。
几点测试结果
- 对象的proto都是等于proto.constructor所指构造函数的prototype。无论是普通对象还是函数对象经测试都是这样,若是Function则proto和prototype都是new Function的实例函数对象,其它函数或构造函数则是自身的实例对象,是普通对象。
- proto是对象的默认原型链,由JS定义的,许多称之为[[prototype]],后来chrome才引入__prototype这种书写方式,开始时候是不可更改的,后来有的网文说是从ES6以后可以更改了,我测试的结果,
无论是普通对象还是函数对象,是可以改变它的值,但原型链改变则限定当前对象,尤其是函数对象的实例则不受影响。- prototype是函数对象的原型链, 它的改变是 直接影响它的实例原型链。
- proto和prototype对原型链影响区别 也许上面2、3点让你迷糊了,说实在话,我在这个难题测试了好久,按说proto改变了函数对象的原型链,那么它的实例应该也随之改变原型链?其实不然,proto和prototype一般情况下可以看成两条平行的原型链,只有在Function时才合二为一,就是
Function.__proto__==Function.prototype,而一般函数对象Person.__proto__==Function.prototype,Person.prototype则是比较特殊,它由两部分组成:constructor为Person,proto为标准对象。
console.dir(Object);console.dir(Object.__proto__==Function.prototype);console.dir(Function);console.dir(Function.__proto__==Function.prototype);function Person(name){this.name=name;this.hello=function(){console.log(`Hello ${this.name}`);};}console.dir(Person);

proto和prototype的使用总结:
- 普通对象而言: 无论是改变它的
__proto__或__proto__.constructor代表的构造函数的prototype,都可以改变原型链。- 函数对象而言: proto只改变当前函数对象的原型链,而prototype是改变它的实例对象的原型链。由于二者是不相等,所以是两条平行的原型链, 前者只影响自己,后者只影响实例对象 。
- 二者总结: 无论是普通对象还是函数对象,
__proto__只影响自己原型链。而prototype则是函数对象独有,它影响以它为构造函数的实例对象原型链。另外 prototype可以扩展原函数对象的成员(属性或方法)为所有实例对象所共享 。一般不建议修改__proto__。
这两个应该是JS的核心概念了,二者都是函数对象,不过都是特殊的函数对象。
Function无限循环吗?
Function特殊在它的原型链指向自身,即Function.__proto__等于Function.prototype即它的构造函数为Function,按正常逻辑这岂不是无限循环了?,JS针对Function进行了特别处理,第一次Function其实仍然相当于用户定义的函数,而它的构造函数则是JS定义的Function了,它的Function.__proto__或Function.prototype.__proto__等于Object.prototype。
就是说: 我们用户永远无法直接调用JS内置的Function函数对象,调用的Function只是它的new Function()出来的函数对象 。

Object是所有对象之父?
通过打印输出,无论是普通对象还是函数对象,最终都是Object.prototype,也是标准对象即new Object()/Object()/{},所以可以说Object是所有对象之父。默认情况下,对象的原型链是:
1、obj.proto==obj.proto.constructor.prototype;
constructor是构造函数,若是函数对象,它的构造函数是Function
2、constructor.prototype.proto=Object.prototype
4、Object.prototype.proto==null。
这里就直接引用老师的案例了。构造函数new后可分三部分:创建空对象并将this指向它,为空对象添加成员,最后返回这个对象。
// 构造函数是用来创建对象/实例function User(name, age) {// 1. 自动生成一个新对象,并用this指向它// this = new User;// 2. 为这个新生成的对象,添加成员,例如属性或方法this.name = name;this.age = age;this.getInfo = function () {return `${this.name} : ${this.age}`;};// 3. 返回这个新对象// return this;}
主要有两种继承方式:类式继承或构造函数继承,另一种就原型链继承,相关介绍文章已经比较多了,这里重点介绍下原型链式继承
function Base(){this.node='node';this.hello=function(){console.log('Hello World');};}Base.prototype.hello2=function(){console.log('Hello World Two');};function Animal(type){// 第三种:构造函数继承// Base.call(this);this.type=type;this.Say=function(){console.log('Animal Say');};}function Dog(name){// 第三种:构造函数继承// Animal.call(this,'crab');this.name=name;}// 第一种:原型继承Animal.prototype=new Base();Dog.prototype=new Animal('crab');// 第二种:原型链// Dog.prototype=Animal.prototype;// Animal.prototype=Base.prototype;const dog=new Dog('Bill');console.dir(Dog);console.dir(dog);// console.dir(dog.Say());console.dir(dog.hello());
三种方式比较:
- 原型链继承:将要继承的直接实例化,赋值给构造函数的prototype。它可以访问要继承的内部成员,也访问函数对象通过prototype定义的成员。是推荐的继承方式。
- 原型继承:将要继承的prototype赋值给构造函数的prototype,直接改变实例对象的原型链 。它只可访问prototype扩展的成员,内部成员无法访问
- 构造继承:通过在构造函数中调用要继承的函数对象call或apply方法,将this指向要继承的函数对象,只能访问内部成员,prototype扩展的成员无法访问。
首先上面第一部分不要求全部理解,其实我上面也解释不是很清楚,只是将自己测试结果和主要的进行了说明,以后再慢慢探讨吧,下面还是实战应用,介绍下prototype两个方面的应用,至于在继承方面的应用可见第一部分介绍了。
如第一部分介绍中所说,如想要给某构造函数添加新成员,则可以通过prototype,它添加的成员(属性或方法)将被它的所有实例对象共享。也许你会说直接在构造函数中定义不就可以了吗?经过测试 它和构造函数定义的成员最大的区别就是可以在new之后 ,如
// 构造函数,当成父类// 构造函数,当成父类function Parent() {this.name = 'admin';}// 我认为它是一个子类function Child() {this.age = 99;}// 函数的原型属性可以被改写, 利用这个特征, 可以轻易实现继承// Child.prototype = null;Child.prototype = new Parent();console.dir(Child);// 使用的时候,直接将子类当成工作类,let instance = new Child();// 原型上声明的成员,会被基于当前构造函数的所有实例所共享Parent.prototype.getName = function () {return this.name;};// 现在还可以给这个子类继续添加成员Child.prototype.getAge = function () {return this.age;};// 访问子类成员console.log(instance.age);console.log(instance.name);console.log(instance.getName());console.log(instance.getAge());
在前面博文中已经演示了Array.prototype.join.call或String.prototype.substr.call等借用技巧,直到今天才来说明它的来源。在JS中内置的String、Array、Number等函数对象中已经定义许多方法,我们通过call或apply可以改变this,从而达到借用方法的效果。

三个方法中的第一个参数都是用来改变this指向。call和apply区别是第二个参数,call是以列表形式传参,而apply是数组形式传参。常用于函数借用或构造函数继承中 。
function f1(a, b, c) {return a + b + c;}obj = { a: 30 };console.log(f2.call(obj, 10, 20));console.log(f2.apply(obj, [10, 20]));
bind与它们还有一些不一样的地方,bind并不是立即调用该函数,而是返回了一个函数的声明,常用于回调函数中。bind用在回调函数中改变this的值 ,因为回调是异步的,需要事件来触发。
document.querySelector("button").addEventListener("click",function () {console.log(this);document.body.appendChild(document.createElement("p")).innerHTML = "欢迎: " + this.name;}.bind({ name: "朱老师" }));
一开始以为reduce应用就是求和,经过老师演示才知道它有那么多应用,主要是自己只满足老师上课所讲,没有深入了解它的语法。下面选看下它的语法
rudece的语法:
arr.reduce(function (prev,curr,index,arr){}, init)
- prev: 存储每步处理的结果。第一次时若没有第二个参数init,则取数组第一个元素,curr从第二个开始;若有第二个元素则等于init,curr从数组第一个开始。
- curr,index, arr,与其它的迭代方法参数功能相同。curr: 当前元素,index当前元素的索引,arr当前元素所在的数组本身。
- init:归并的初始值,即是第一次时prev的值。
let arr = [1, 2, 3, 4, 5];console.log('----没有init参数时----');arr.reduce((prev, curr, index, arr) => {console.log(prev, curr, index, arr);return prev + curr;});console.log('----有init参数时----');arr.reduce((prev, curr, index, arr) => {console.log(prev, curr, index, arr);return prev + curr;}, 100);
;
let arr = [1, 2, 3, 4, 5];console.log('和=',arr.reduce((prev, curr) => {return prev + curr;}));console.log('最大值=',arr.reduce((prev, curr) => {return Math.max(prev, curr);}));
let arr = [2, 3, 3, 4, 5, 4, 5, 5, 6, 2, 3, 3, 5];function arrayCount(arr, value) {return arr.reduce((total, item) => (total += item == value ? 1 : 0), 0);}console.log('3 出现的次数: ', arrayCount(arr, 3));console.log('5 出现的次数: ', arrayCount(arr, 5));console.log('2 出现的次数: ', arrayCount(arr, 2));
将去掉重复值的元素组成一个新数组返回,所以将返回的结果设置一个空数组
let arr = [2, 3, 3, 4, 5, 4, 5, 5, 6, 2, 3, 3, 5];let res = arr.reduce((prev, curr) => {if (prev.includes(curr) === false) prev.push(curr);return prev;}, []);console.log(res);
const items = [{ id: 1, name: '手机', price: 4500, num: 3 },{ id: 2, name: '电脑', price: 6500, num: 5 },{ id: 3, name: '汽车', price: 15500, num: 2 },{ id: 4, name: '相机', price: 19500, num: 9 },{ id: 4, name: '耳机', price: 26800, num: 9 },];// 商品数量之和, 注意一定要传第二个参数,给最终结果赋初会值: 0, 这很重要let counts = items.reduce((total, item) => total + item.num, 0);console.log(`总数量:`, counts);// 商品总金额, 注意传第二个参数,否则会得到一个数字字符串let amounts = items.reduce((total, item) => total + item.num * item.price, 0);console.log(`总金额:`, amounts);// 给每个商品套个html标签res = items.map(item =>`<tr><td>${item.id}</td><td>${item.name}</td><td>${item.price}</td><td>${item.num}</td><td>${item.price * item.num}</td></tr>`);// 将每个商品归并到一个html字符串中let content = res.reduce((prev, item) => prev.concat(item));// 使用表格将代码渲染到页面上const table = document.createElement('table');// 标题table.innerHTML += '<caption>商品信息表</caption>';// 表头table.innerHTML += `<thead><tr><th>编号</th><th>商品</th><th>单价</th><th>数量</th><th>金额/元</th></tr></thead>`;// 将动态生成的内容添加到表格中table.innerHTML += `<tbody>${content}</tbody>`;table.innerHTML += `<tfoot><tr><td colspan="3">总计:</td><td>${counts}</td><td>${amounts}</td></tr>`;// 做为body第一个子元素插入到页面中document.body.insertBefore(table, document.body.firstElementChild);
尽管本文未对JS的proto和prototype有较彻底的测试,便相比以前对JS的原型链有了更多认识,也基本了解的JS的万物皆是对象的本质,更加完善只能等以后有更多认识再补充了。另一个就是要注重语法,从中可以发现更多的应用,reduce如此,数组的map和filter也可要类似学习。
补充:从ES6开始使用class语法糖来模拟类了,对于继承来说,理解更一目了然。继承是extends,在继承类的构造constructor中通过super()来继承父类。导出使用export,导入是import。记得在某个视频教程中推荐这种做法,而且在vue中已经使用上了,随处可见import、export、export default等关键字。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号