Blogger Information
Blog 54
fans 6
comment 31
visits 107520
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
JS的原型探讨和数组函数reduce再学习
吾逍遥
Original
854 people have browsed it

这个博文是我测试最痛苦的,它涉及太底层了,老师给我打开了prototype的大门,但如何深入理解则查阅了大量的网文,尤其是原型链和继承的探索,让我不断反复推翻自己的结论,现在博文也不算完美,不过是目前我最高的理解了,以后更深入再说吧。

一、JS的原型链

1、JS中万物皆是对象

JS中万物皆是对象,应该是JS中最经典的口头禅了,但是在我测试Object和Function等内置对象时,则出现深深的疑惑,先看测试结果

  1. console.dir(Object);
  2. console.log('Object => ', Object.prototype.toString.call(Object));
  3. console.dir(Function);
  4. console.log('Function => ', Object.prototype.toString.call(Function));
  5. console.dir(Number);
  6. console.log('Number => ', Object.prototype.toString.call(Number));

object

网上普遍解释: JS中对象概念应该分为两种:一是普通对象,另一个是函数对象。凡是通过new Function()创建的对象都是函数对象,其他的都是普通对象。function(){}实质也是调用new Function()来创建的。

至少是我理解JS的万物皆是对象是指数据类型的对象,其实不然,还是先看下面测试吧

  1. function Person(name){
  2. this.name=name;
  3. this.hello=function(){console.log(`Hello ${this.name}`);};
  4. }
  5. console.log(Object.prototype.toString.call(123));
  6. console.log(Object.prototype.toString.call('abc'));
  7. console.log(Object.prototype.toString.call(true));
  8. console.log(Object.prototype.toString.call(undefined));
  9. console.log(Object.prototype.toString.call(null));
  10. console.log(Object.prototype.toString.call([]));
  11. console.log(Object.prototype.toString.call(Person));
  12. console.log(Object.prototype.toString.call({}));

object-type

相信从上图中应该能发现点什么,也是图中我以前没注意,而在测试时才注意到的细节,就是通过Object.prototype.toString.call显示数据类型时,总是由两部分组成:小写object和大写开头的数据类型。

所谓JS万物皆是对象,依据就是前面的object,它指明数据特性,就是说数据都是对应的Number、String、Boolean…Function和Object等函数的实例。而数据类型中对象则是由Object()创建的实例。

普通对象和函数对象: 函数对象就是指object Function,其它则是普通对象,前者拥有proto和prototype两个重要属性,后者只拥有proto属性,数值、字符串等都拥有proto属性喔,现在应该更理解JS万物皆是对象的说法了。

object-type2

2、__proto__、prototype和constructor

上面已经说到普通对象有proto属性,而函数对象还具有prototype属性,constructor是构造函数,是二者属性下的重要属性,也是理解原型链的关键。

几点测试结果

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

object-proto

proto和prototype的使用总结:

  • 普通对象而言: 无论是改变它的__proto__或__proto__.constructor代表的构造函数的prototype,都可以改变原型链。
  • 函数对象而言: proto只改变当前函数对象的原型链,而prototype是改变它的实例对象的原型链。由于二者是不相等,所以是两条平行的原型链, 前者只影响自己,后者只影响实例对象
  • 二者总结: 无论是普通对象还是函数对象,__proto__只影响自己原型链。而prototype则是函数对象独有,它影响以它为构造函数的实例对象原型链。另外 prototype可以扩展原函数对象的成员(属性或方法)为所有实例对象所共享一般不建议修改__proto__

3、Object和Function

这两个应该是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()出来的函数对象

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。

4、构造函数的new

这里就直接引用老师的案例了。构造函数new后可分三部分:创建空对象并将this指向它,为空对象添加成员,最后返回这个对象

  1. // 构造函数是用来创建对象/实例
  2. function User(name, age) {
  3. // 1. 自动生成一个新对象,并用this指向它
  4. // this = new User;
  5. // 2. 为这个新生成的对象,添加成员,例如属性或方法
  6. this.name = name;
  7. this.age = age;
  8. this.getInfo = function () {
  9. return `${this.name} : ${this.age}`;
  10. };
  11. // 3. 返回这个新对象
  12. // return this;
  13. }

5、继承和原型链

主要有两种继承方式:类式继承或构造函数继承,另一种就原型链继承,相关介绍文章已经比较多了,这里重点介绍下原型链式继承

  1. function Base(){
  2. this.node='node';
  3. this.hello=function(){console.log('Hello World');};
  4. }
  5. Base.prototype.hello2=function(){console.log('Hello World Two');};
  6. function Animal(type){
  7. // 第三种:构造函数继承
  8. // Base.call(this);
  9. this.type=type;
  10. this.Say=function(){console.log('Animal Say');};
  11. }
  12. function Dog(name){
  13. // 第三种:构造函数继承
  14. // Animal.call(this,'crab');
  15. this.name=name;
  16. }
  17. // 第一种:原型继承
  18. Animal.prototype=new Base();
  19. Dog.prototype=new Animal('crab');
  20. // 第二种:原型链
  21. // Dog.prototype=Animal.prototype;
  22. // Animal.prototype=Base.prototype;
  23. const dog=new Dog('Bill');
  24. console.dir(Dog);
  25. console.dir(dog);
  26. // console.dir(dog.Say());
  27. console.dir(dog.hello());

三种方式比较:

  1. 原型链继承:将要继承的直接实例化,赋值给构造函数的prototype。它可以访问要继承的内部成员,也访问函数对象通过prototype定义的成员。是推荐的继承方式。
  2. 原型继承:将要继承的prototype赋值给构造函数的prototype,直接改变实例对象的原型链 。它只可访问prototype扩展的成员,内部成员无法访问
  3. 构造继承:通过在构造函数中调用要继承的函数对象call或apply方法,将this指向要继承的函数对象,只能访问内部成员,prototype扩展的成员无法访问。

二、prototype的应用

首先上面第一部分不要求全部理解,其实我上面也解释不是很清楚,只是将自己测试结果和主要的进行了说明,以后再慢慢探讨吧,下面还是实战应用,介绍下prototype两个方面的应用,至于在继承方面的应用可见第一部分介绍了。

1、为构造函数增加共享的的成员(属性和方法)

如第一部分介绍中所说,如想要给某构造函数添加新成员,则可以通过prototype,它添加的成员(属性或方法)将被它的所有实例对象共享。也许你会说直接在构造函数中定义不就可以了吗?经过测试 它和构造函数定义的成员最大的区别就是可以在new之后 ,如

  1. // 构造函数,当成父类
  2. // 构造函数,当成父类
  3. function Parent() {
  4. this.name = 'admin';
  5. }
  6. // 我认为它是一个子类
  7. function Child() {
  8. this.age = 99;
  9. }
  10. // 函数的原型属性可以被改写, 利用这个特征, 可以轻易实现继承
  11. // Child.prototype = null;
  12. Child.prototype = new Parent();
  13. console.dir(Child);
  14. // 使用的时候,直接将子类当成工作类,
  15. let instance = new Child();
  16. // 原型上声明的成员,会被基于当前构造函数的所有实例所共享
  17. Parent.prototype.getName = function () {
  18. return this.name;
  19. };
  20. // 现在还可以给这个子类继续添加成员
  21. Child.prototype.getAge = function () {
  22. return this.age;
  23. };
  24. // 访问子类成员
  25. console.log(instance.age);
  26. console.log(instance.name);
  27. console.log(instance.getName());
  28. console.log(instance.getAge());

2、借用JS内置的函数对象的方法

在前面博文中已经演示了Array.prototype.join.call或String.prototype.substr.call等借用技巧,直到今天才来说明它的来源。在JS中内置的String、Array、Number等函数对象中已经定义许多方法,我们通过call或apply可以改变this,从而达到借用方法的效果。

string

3、call、apply和bind的应用

三个方法中的第一个参数都是用来改变this指向。call和apply区别是第二个参数,call是以列表形式传参,而apply是数组形式传参。常用于函数借用或构造函数继承中

  1. function f1(a, b, c) {
  2. return a + b + c;
  3. }
  4. obj = { a: 30 };
  5. console.log(f2.call(obj, 10, 20));
  6. console.log(f2.apply(obj, [10, 20]));

bind与它们还有一些不一样的地方,bind并不是立即调用该函数,而是返回了一个函数的声明,常用于回调函数中。bind用在回调函数中改变this的值 ,因为回调是异步的,需要事件来触发。

  1. document.querySelector("button").addEventListener(
  2. "click",
  3. function () {
  4. console.log(this);
  5. document.body.appendChild(document.createElement("p")).innerHTML = "欢迎: " + this.name;
  6. }.bind({ name: "朱老师" })
  7. );

三、数组方法reduce再学习

一开始以为reduce应用就是求和,经过老师演示才知道它有那么多应用,主要是自己只满足老师上课所讲,没有深入了解它的语法。下面选看下它的语法

rudece的语法: arr.reduce(function (prev,curr,index,arr){}, init)

  • prev: 存储每步处理的结果。第一次时若没有第二个参数init,则取数组第一个元素,curr从第二个开始;若有第二个元素则等于init,curr从数组第一个开始。
  • curr,index, arr,与其它的迭代方法参数功能相同。curr: 当前元素,index当前元素的索引,arr当前元素所在的数组本身。
  • init:归并的初始值,即是第一次时prev的值。
  1. let arr = [1, 2, 3, 4, 5];
  2. console.log('----没有init参数时----');
  3. arr.reduce((prev, curr, index, arr) => {
  4. console.log(prev, curr, index, arr);
  5. return prev + curr;
  6. });
  7. console.log('----有init参数时----');
  8. arr.reduce((prev, curr, index, arr) => {
  9. console.log(prev, curr, index, arr);
  10. return prev + curr;
  11. }, 100);

reduce;

1、求和或最大值

  1. let arr = [1, 2, 3, 4, 5];
  2. console.log('和=',arr.reduce((prev, curr) => {
  3. return prev + curr;
  4. }));
  5. console.log('最大值=',arr.reduce((prev, curr) => {
  6. return Math.max(prev, curr);
  7. }));

2、统计某个元素的出现的频率/次数

  1. let arr = [2, 3, 3, 4, 5, 4, 5, 5, 6, 2, 3, 3, 5];
  2. function arrayCount(arr, value) {
  3. return arr.reduce((total, item) => (total += item == value ? 1 : 0), 0);
  4. }
  5. console.log('3 出现的次数: ', arrayCount(arr, 3));
  6. console.log('5 出现的次数: ', arrayCount(arr, 5));
  7. console.log('2 出现的次数: ', arrayCount(arr, 2));

3、数组去重

将去掉重复值的元素组成一个新数组返回,所以将返回的结果设置一个空数组

  1. let arr = [2, 3, 3, 4, 5, 4, 5, 5, 6, 2, 3, 3, 5];
  2. let res = arr.reduce((prev, curr) => {
  3. if (prev.includes(curr) === false) prev.push(curr);
  4. return prev;
  5. }, []);
  6. console.log(res);

4、快速生成html代码并渲染到页面中

  1. const items = [
  2. { id: 1, name: '手机', price: 4500, num: 3 },
  3. { id: 2, name: '电脑', price: 6500, num: 5 },
  4. { id: 3, name: '汽车', price: 15500, num: 2 },
  5. { id: 4, name: '相机', price: 19500, num: 9 },
  6. { id: 4, name: '耳机', price: 26800, num: 9 },
  7. ];
  8. // 商品数量之和, 注意一定要传第二个参数,给最终结果赋初会值: 0, 这很重要
  9. let counts = items.reduce((total, item) => total + item.num, 0);
  10. console.log(`总数量:`, counts);
  11. // 商品总金额, 注意传第二个参数,否则会得到一个数字字符串
  12. let amounts = items.reduce((total, item) => total + item.num * item.price, 0);
  13. console.log(`总金额:`, amounts);
  14. // 给每个商品套个html标签
  15. res = items.map(
  16. item =>
  17. `<tr>
  18. <td>${item.id}</td>
  19. <td>${item.name}</td>
  20. <td>${item.price}</td>
  21. <td>${item.num}</td>
  22. <td>${item.price * item.num}</td>
  23. </tr>`
  24. );
  25. // 将每个商品归并到一个html字符串中
  26. let content = res.reduce((prev, item) => prev.concat(item));
  27. // 使用表格将代码渲染到页面上
  28. const table = document.createElement('table');
  29. // 标题
  30. table.innerHTML += '<caption>商品信息表</caption>';
  31. // 表头
  32. table.innerHTML += `
  33. <thead>
  34. <tr>
  35. <th>编号</th>
  36. <th>商品</th>
  37. <th>单价</th>
  38. <th>数量</th>
  39. <th>金额/元</th>
  40. </tr>
  41. </thead>`;
  42. // 将动态生成的内容添加到表格中
  43. table.innerHTML += `<tbody>${content}</tbody>`;
  44. table.innerHTML += `<tfoot><tr><td colspan="3">总计:</td><td>${counts}</td><td>${amounts}</td></tr>`;
  45. // 做为body第一个子元素插入到页面中
  46. 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等关键字。

Correcting teacher:天蓬老师天蓬老师

Correction status:qualified

Teacher's comments:
Statement of this Website
The copyright of this blog article belongs to the blogger. Please specify the address when reprinting! If there is any infringement or violation of the law, please contact admin@php.cn Report processing!
All comments Speak rationally on civilized internet, please comply with News Comment Service Agreement
2 comments
吾逍遥 2020-12-01 05:17:16
从es6开始,有了class,尽管是语法糖,还是好理解多了
2 floor
天蓬老师 2020-11-30 17:52:49
同学, 忘了原型及原型链吧, 以后是class的世界 , 尽管class是prototype的语法糖,但又如何?
1 floor
Author's latest blog post