博客列表 >JS的原型探讨和数组函数reduce再学习

JS的原型探讨和数组函数reduce再学习

吾逍遥
吾逍遥原创
2020年12月01日 07:31:30859浏览

这个博文是我测试最痛苦的,它涉及太底层了,老师给我打开了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等关键字。

声明:本文内容转载自脚本之家,由网友自发贡献,版权归原作者所有,如您发现涉嫌抄袭侵权,请联系admin@php.cn 核实处理。
全部评论
文明上网理性发言,请遵守新闻评论服务协议
吾逍遥2020-12-01 05:17:162楼
从es6开始,有了class,尽管是语法糖,还是好理解多了
天蓬老师2020-11-30 17:52:491楼
同学, 忘了原型及原型链吧, 以后是class的世界 , 尽管class是prototype的语法糖,但又如何?