JS中的闭包及面向对象编程 — 2、面向对象编程详解

545 阅读12分钟

1、大纲

单例模式(Singleton Pattern)

  • 单例模式由来和作用
  • 模块化开发
  • 高级单例模式
  • ...

面向对象(oop)

  • 对象、类、实例

  • 浏览器中常用的内置类

    数据类型

    DOM元素

    ...

  • 学习面向对象的意义

构造函数模式(Constructor)

  • 使用构造函数创建数据值

    创建基本数据类型

    创建对象、数组、正则、函数

  • 使用构造函数创建自定义类

    对比和普通函数执行的区别

    构造函数体中的私有变量和私有属性

    构造函数体中的return

    instanceof

    hasOwnProperty

    in

    重写hasPubProperty

原型和原型链(prototype)

  • 关于一些内置属性

    prototype

    ——proto——

    constructor

    哪些是函数?哪些是对象?

    ...

  • 原型链

    原型链查找机制

    原型链中的this问题

    批量扩展原型上的方法

    基于内置类原型扩展方法(链式写法)

    ...

  • 类继承

    继承、封装、多态

    原型继承

    call继承

    寄生组合继承

    ES6中的类及类的继承

2、单例设计模式的由来和作用

单例模式

在真实项目中,为了实现模块化开发或者团队协作开发,我们经常应用单例模式(一般业务逻辑部分的代码都是依托单例模式设计规划的)

单例模式的由来

long long ago~~,js中都是值类型,没有引用数据类型

var name='李芙蓉';
var age=13;
var sex='女神';

var name='赵淼';
var age=81;
var sex='未知';

// =>如果后面编写的代码,创建的变量或者函数名和之前的一样,会把之前存储的
//   值替换掉;真实项目中,团队协作开发,如果是这样来处理,经常会导致相互
//   代码的冲突;‘全局变量污染’‘全局变量冲突’

后来js中诞生了对象数据类型,解决了上面出现的污染或者冲突问题


// =>把描述同一件事物的特征或者属性,进行归纳汇总(放在一起),以此来避免全局变量之间的冲突
var personl={
    name:"李芙蓉",
    age:13,
    sex:'小仙女',
};

var person2={
    name:"赵淼",
    age:81,
    sex:'看着办',
};
personl.age
person2.name

我们把对象数据类型实现把描述同一件事物的属性或者特征归纳汇总在一起,以此避免全局变量冲突问题的方式和思想叫做:单例设计模式

//=>单例模式
//1、singleton不仅仅是对象名了,在单例模式中,singleton称之为‘命名空间(nameSpace)’
var singleton={
    xxx:xxx,
    ...
};


var singleton={
    nameSpace1:{
        xxx:xxx,
        ...
    },
    nameSpace2:{
        xxx:xxx,
        ...
    }
    ...
};
singleton.nameSpace1.xxx

把描述同一件事物的属性或者方法存放在某一个命名空间下,多个命名空间中的属性和方法是互不干扰的

3、使用单例模式实现模块化开发

模块化开发:在团队协作开发的时候,我们经常会把一个复杂页面,按照具体的功能划分成为几大块,然后分别去开发,这种模块划分的思想就是模块化开发思想

真实项目中,我们可以使用单例模式(建议也是使用单例模式)来实现模块化开发

//=>项目主管(开发人员):公共模块
var utils={
    trim:function(){},
    ...
}

//=>A:搜索模块
var searchModel={
    submit:function(){
        utils.trim();
    },
    ...
};

//=>B:天气模块
var weatherModel={
    setWeather:function(){},
    ...
};

//=>C:频道模块
var channelModel={
    show:function(){
        //=>在当前的命名空间下调取其它命名空间的方法:指定好对应的命名空间名字即可,
        //  使用 [NameSpace].[property] 就可以操作了
        searchModel.submit();

        // channelModel.setChannel();
        //=>调取本模块中的一些方法,可以直接使用this处理即可:此方法中的this一般都是
        //  当前模块的命名空间
        this.setChannel();
    },
    setChannel:function(){},
    ...
};
channelModel.show();

4、高级单例模式

基于js高阶编程技巧‘惰性思想’来实现的单例模式(利用了闭包的保存机制),并且可以把一些常用的设计模式(例如:命令模式发布订阅模式promise设计模式等)融合进来,最后清晰的规划我们的业务逻辑代码,方便后期二次开发和维护,这种设计思想综合体就是高级单例模式,也是项目中最常用的

var searchModel=(function(){
    function submit(){
        ...
    }

    return{
        submit:submit
    }
})();
searchModel.submit();
//=>一般不像上面那样写,写法如下(融合了命令模式)----------
var searchModel=(function(){
    function submit(){
        ...
    }

    function fn(){
        ...
    }

    return{
        init:function(){
            // submit:submit
            this.submit();
            this.fn();
        }
    }
})();
searchModel.init();
//=>还有种写法如下(融合了命令模式和发布订阅模式),也是真正项目中的写法-----
//=>后期还会涉及到ajax的一些处理,ajax嵌套处理还会融入promise涉及模式
//=>也就是把发布订阅、命令、promise模式全部融入在这些业务逻辑里面,由最终一个
//  单例模式进行整体的挂载(高级单例模式的扩展性很好,可以融合很多其他的模式,最终
//  汇合在一起)
var searchModel=(function(){
    var $searchPlan=$.Callbacks()//jq里面提供的方法

    function submit(){
        ...
    }
    $searchPlan.add(submit);//执行submit()

    function fn(){
        ...
    }
    $searchPlan.add(fn);

    return{
        init:function(){
            // this.submit();
            // this.fn();
            $searchPlan.fire();//通知计划表中的方法执行
        }
    }
})();
searchModel.init();

面试让你写一个单例模式或者问你什么是单例模式:(以上代码中最后一种写法)

一个单例模式,可以把“命令模式”、“发布订阅模式”、“promise设计模式”、“惰性思想”全部写出来,发布订阅可以更加清晰的规划业务逻辑,可以把一些事件事先存储在我们的事件池当中,到达执行时间的时候只需要通知就可以了,这样的话就不用再写一个方法再执行里面的代码了,这种模式特别好,除了这个以外,在之前封装框架的时候也是用的发布订阅模式来做的,这样的更容易维护框架里的代码。惰性思想就简单了,就js里面的一个小模式小思想而已,就是模块保存利用闭包机制,然后命令模式在业务逻辑里面我们可以把它作为一个业务口,这样的话通过一些参数配置来进行规划,特别好也特变方便,然后promise模式,当代码嵌套的时候,用promise管理的话实现起来特别方便,(每个东西说出来并且答到点上);promise原理...之前也自己写过promise库,大概怎么实现的...

5、初步理解面向对象(OOP)

面向对象是编程思想(面向过程编程思想:c语言是面向过程的),java、php、c#、c++、.net(dot net)、js...这些都是面向对象编程的

html和css是标记语言不是编程语言,没有所谓的面向对象编程

后期学习的LESS/SASS属于CSS预编译语言,旨在把CSS变为编程语言(面向对象)

对象、类、实例

对象:编程语言中的对象是一个泛指,万物皆对象(我们所要研究学习以及使用的都是对象)

类:对象的具体细分(按照属性或者特性细分为一些类别)

实例:某一类中具体的事物

[实际生活中]

自然界中万物皆对象,我们为了认知自然界,我们会把自然界中的事物按照特征进行分类,例如:

动物类

  • 人类

    正常人类

    程序猿类

  • 爬行动物类

  • 哺乳动物类

  • ...

植物类

微生物类

...

周老师就是程序猿类别中的一个实例

js本身就是基于面向对象创造出来的语言(所以他是面向对象编程),我们想要学习js,首先也是给其进行分类,浏览器已经把js按照类别分好了,我们拿出某一类中的实例,进行学习和研究

6、js中一些常用的内置类

js中常用内置类

关于数据类型的

  • Number:每一个数字或者NaN是它的一个实例

  • String:字符串类

  • Null

  • Undefined:浏览器屏蔽了我们操作 Null 或者 Undefined 这个类

  • Object:对象类,每一个对象数据类型都是它的实例

    Array:数组类

    RegExp:正则类

    Date:日期类

    ...

  • Function:函数类,每一个函数都是它的一个实例

关于元素对象和元素集合的

  • HTMLCollection:元素集合类
  • NodeList:节点集合类
  • HTMLDivElement
  • HTMLElement
  • Element
  • Node
  • EventTarget
  • ...

//=>元素集合类
getElementsByTagName
getElementsByClassName
querySelectorAll

//=>节点集合类
getElementsByName
childNodes

7、面向对象学习对本阶段的一些意义

研究数组

1、创建一个数据类的实例,研究其基础语法和结构

2、如果想要研究数组具备的功能方法,我们只需要看Array/Object这些类上都赋予了它什么样的方法

数组的所有方法如下:(每个方法都可以研究)

[问题]

document.getElementById它的上下文只能是document,其它不可以?

答:因为getElementById这个方法只有Document这个类才有,其它的类没有,所以只有document这个作为Document的实例才能使用这个方法

8、使用构造函数方法创建数据类型值

基于面向对象创建数据值

注意数组,对象,基本数据类型的三种区别。

var ary=[12,23];//=> 字面量创建方式

//=>严谨的基于面向对象(构造函数)方式创建一个数组
var ary=new Array();

// 两种创建方式在核心意义上没有差别,都是创建Array这个类的一个实例,
// 但是在语法上是有区别的
// 1、字面量创建方式传递进来什么,都是给数组每一项加入的内容
// 2、构造函数创建方式
//    new Array():创建一个长度为10的数组,数组中的每一项都是空
//    new Array('10'):如果只传递一个实参,并且实参不是数字,相当于把当前值作为数组的第一项存进来
//    new Array(10,20,30):如果传递多个实参,不是设置长度,而是把传递的内容当做数组中的每一项存储起来

//-------------------------------------------------------------------------------------------------

var obj={name:'珠峰培训'};
var obj=new Object();//=>一般只用于创建空对象,如果需要增加键值对,创建完成后自己一次添加即可


//-------------------------------------------------------------------------------------------------

var num=12;//=>字面量创建出来的是一个基本数据类型值Number(但是也是Number的一个实例=>浏览器帮
// 我们构造出来的Number实例,可以调取Number赋予它的方法),num.toFixed:就是Number赋予它的方法

var num=new Number(12);//=>构造函数方式创建出来的也是Number的一个实例(也可以使用Number赋予它的方法),
//但是获取的结果是对象Object数据类型的

//-------------------------------------------------------------------------------------------------

9、构造函数和普通函数执行的区别

构造函数设计模式(constructor)

使用构造函数方式,主要是为了创建类和实例的,也就是基于面向对象编程思想来实现一些需求的处理

在js中,当我们使用"new xxx()"执行函数的时候,此时的函数就不是普通的函数了,而是变为了一个类,返回的结果叫做当前的类的实例,我们这种new xxx执行的方式称之为‘构造函数设计模式’

function fn(){
    ...
};
new fn();//=>fn是一个类,f是当前这个类的一个实例 ‘构造函数设计模式’ (我们
//一般都会把类名第一个字母大写)

普通函数执行 vs 构造函数执行

普通函数执行

1、开辟一个新的私有作用域

2、形参赋值

3、变量提升

4、代码自上而下执行(return后面的值就是当前函数返回的结果)

5、栈内存释放或者不释放问题

function fn(num){
    this.num=num;//=>this:window 相当于给全局对象增加一个num的属性名,属性值是10
    var total=null;
    total+=num;
    return total;
}
fn(10);//=>f:10

构造函数执行

1、首先和普通函数执行一样,也需要开辟一个新的私有作用域

2、在私有作用域中完成类似于普通函数的操作:形参赋值以及变量提升

3、在代码自上而下执行之前,构造函数有属于自己比较特殊的操作:浏览器会在当前的作用域中默认创建一个对象数据类型的值,并且会让当前函数中的this指向创建的这个对象

4、像普通函数一样,代码自上而下执行:this.xxx=xxx这里操作都是在给创建的这个对象增加属性名和属性值

5、代码执行完成后,即使函数中没有写return,在构造函数模式中:浏览器会默认的把创建的对象返回到函数的外面

构造函数执行,即具备普通函数执行的一面,也同时具备自己独有的一些操作;

在构造函数执行期间,浏览器默认创建的对象(也就是函数体中的this)就是当前这个类的一个实例,浏览器会把默认创建的实例返回,所以我们说:new Fn()执行,Fn是一个类,返回的结果是Fn这个类的一个实例

function Fn(num){
    //=>在构造函数模式中,方法体中出现的this是当前类的一个实例
    //=>(this.xxx=xxx都是在给当前实例增加一些私有的属性)
    this.num=num;
}
var f = new Fn(10);

blog.csdn.net/caoxinhui52… 参照Javascript中的new做了什么?这个看看

10、构造函数操作的深入理解

深入理解构造函数执行的步骤

1、 当构造函数或者类,执行的时候不需要传递任何的实参值,此时我们是否加小括号就不重要了(不传递实参的情况下,小括号可以省略)

2、构造函数执行,同时具备了普通函数执行的一面,也有自己特殊的一面,但是和实例相关的,只有自己特殊的一面才相关(也就是this.xxx=xxx才相当于给当前实例增加的私有属性),函数体中出现的私有变量,和实例都没有直接的关系、

3、通过类创建出来的每一个实例都是单独的个体(单独的堆内存空间),实例和实例之间是不相同并且独立互不影响的(市面上部分开发把这种模式叫做单例模式,这种说法是错的,js中这种模式叫做构造函数设计模式)

4、在构造函数体中,通过this.xxx给实例设置的属性都是当前实例的‘私有属性’,每个实例之间的私有属性互不干扰

function Fn(){
    var num=100;
    this.name='哈哈哈哈';
    this.sun=function(){};
}
var f1 = new Fn(); // => Fn是类 f1是实例
var f2 = new Fn; // => Fn是类 f2是实例

//=>私有变量num和实例没关系
console.log(f1.num);//=>undefined
console.log(f1.name);//=>'哈哈哈哈'

//=>不同实例是不同的空间地址
console.log(f1===f2);//false
console.log(f1.sum===f2.sum);//=>false

5、当构造函数体中我们自己手动的设置了return(默认返回的是实例:对象类型值),return的是一个基本类型值,对最后返回的实例没有任何的影响,但是如果返回的是引用数据类型的值,会把默认返回的实例替换掉

function Fn(){
    this.name='哈哈哈哈';
    return 10;
}
var f = new Fn(); // => f依然是当前类的一个实例
//---------------------------------------------
function Fn(){
    this.name='哈哈哈哈';
    return {name:'xxx'};
}
var f = new Fn(); // => f不再是Fn的实例,而是变为手动返回的对象了

11、instanceof

用来检测当前实例是否隶属于某个类

function Fn(){}
var f = new Fn;
console.log(f instanceof Fn);//=>true

instanceof解决了typeof无法识别是数组还是正则的问题

[] instanceof Array; //true
/^$/ instanceof Array; //false

12、原型链上的查找机制

三句话一张图搞定(见15)

13、hasOwnProperty

hasOwnProperty vs in

in:用来检测当前这个属相是否隶属于对象(不管是对象私有的还是公有的属性,只要有返回的就是true,相当于检测原型链,只要通过原型链_proto_能找到的属性或者方法都返回true)

hasOwnProperty:用来检测当前这个属相是否是对象的私有属性(不仅要是对象的属性,而且需要是私有的才可以,相当于检测原型prototype,只要对象的原型里面有这个属性或者函数就返回true)

var obj={name:'珠峰培训',age:8};
'name' in obj; //=>true
'sex' in obj; //=>false
'hasOwnProperty' in obj; //=>true hasOwnProperty是Object这个内置类中
//提供的属性方法,只要当前对象obj是Object的一个实例,就可以使用这个方法

obj.hasOwnProperty('name') //=>true
obj.hasOwnProperty('hasOwnProperty') //=>false hasOwnProperty是Object提供的一个公有属性

图解如下:

hasOwnProperty进一步证明 num 是私有属性

function Fn(){
    this.num=100;
}
var f=new Fn();
f.hasOwnProperty('num')//true(进一步证明num是私有属性) f也是Object的一个实例(所有的对象数据类型
// 都是Object这个类的一个实例),所以Object提供的方法都可以使用

hasPubProperty

检测一个属性是否是当前对象的公有属性

1、是对象的一个属性

2、不是对象的私有属性

写一个方法hasPubProperty => 检测当前属性是不是属于这个对象的公有属性

function hasPubProperty(atte,obj){
    return (attr in obj) && (obj.hasOwnProperty(attr)===false)
}
hasPubProperty('hasOwnProperty',{xxx:'xxx'});

14、函数和对象类型汇总

js中的对象和函数汇总

对象数据类型

  • {} 普通对象
  • [] 数组
  • /^$/ 正则
  • Math 数学函数
  • 一般类的实例都是对象数据类型的(除了几个基本数据类型当用字面量创建时,实例是基本类型外=>见8)
  • 函数的prototype属性
  • 实例的_proto_属性
  • ...

函数数据类型值

  • 普通函数
  • 所有的类(内置类和自定义类)
  • ...

15、原型的基础操作(核心操作原理)

原型(三句话)

1、所有的函数都天生自带一个属性:prototype(原型),它是一个对象数据类型的值,在当前prototype对象中,存储了类需要给其实例使用的公有的属性和方法(类的原型上的东西就是它公有的,this.xxx就是它私有的)

2、prototype这个对象,浏览器会默认为其开一个堆内存,在这个堆内存中天生自带一个属性:constructor(构造函数),这个属性存储的值就是当前函数本身

3、每一个类的实例(每一个对象)都天生自带一个属性:_proto_(原型链),属性值是当前对象所属类的原型(prototype)

function Fn(name,age){
    this.name=name;
    this.age=age;
    this.say=function(){
        console.log('my name is'+this.name+'!i am'+this.age+'years old!');
    }
}
Fn.prototype.say=function(){
    console.log('helo world~')
}
Fn.prototype.eat=function(){
    console.log('i love food~')
}
var f1=new Fn('王燕燕',19);
var f2=new Fn('王雪超',69);

=> 1、 Fn是一个函数,开辟一个堆内存存储代码字符串,所有函数类型都天生自带属性prototype

=> 2、prototype是对象数据类型的,浏览器会给它开辟一个堆内存,并且有一个天生自带的属性constructor,constructor存的是当前函数Fn本身(只要有prototype就会有constructor)

=> 3、在Fn原型prototype上加了两个方法say和eat

=> 4、实例f1 是对象数据类型的,f1=new Fn('王燕燕',19),new Fn()这个过程会首先会去执行Fn('王燕燕',19),Fn()里面的this就是实例f1,所以this.xxx就是给这个实例f1增加私有属性xxx

=> 5、实例f2 是对象数据类型的f2=new Fn('王雪超',69),new Fn()这个过程会首先会去执行Fn('王雪超',69),Fn()里面的this就是实例f2,所以this.xxx就是给这个实例f2增加私有属性xxx

=> 6、每个对象数据类型都天生自带_proto_属性,这个属性就是当前它所属类的原型prototype(这里Fn.prototype、f1、f2都是对象)

=> 7、由于f1和f2都是Fn的实例,所以它们的所属类都是Fn,所以f1和f2的_proto_属性就是Fn.prototype(Fn的原型prototype)

所以Fn原型上的方法是公共属性,因为f1和f2都能找到(只要是Fn的实例都可以通过_proto_找到),但是f1和f2里面的属性方法是私有的

结论=>原型的作用:实例(f1、f2)所使用的公有的属性和方法(say、eat)都在类(Fn)的原型上

Fn.prototype也是一个对象,这个对象是new谁出来的?(这里f1和f2都是new Fn()出来的,所以它们是Fn的实例)但是Fn.prototype不知道是new谁出来的,不过由于所有的对象都是Object的一个实例

=> 8、Object类是函数数据类型的,所以开辟一个堆内存,存储代码字符串(这里叫做native code:很多内置的代码字符串),所有的函数都天生自带一个属性prototype

=> 9、Object的prototype也是对象数据类型的,所以浏览器给它开辟一个堆内存,有一个天生自带的属性constructor指向函数本身Object,这是一个内置类,里面会存储很多内置属性和方法(比如:tostring,hasOwnProperty),所有的对象数据类型都天生自带_proto_属性,所以它也会有一个_proto_属性

=> 10、Fn.prototype是一个对象,由于所有的对象数据类型都是Object的一个实例,所以它是Object的一个实例,是Object的所属类;由于所有对象的_proto_都指向当前对象所属类的原型(prototype),所以Fn的_proto_指向Object的原型prototype

=> 11、Object.prototype也是一个对象,但是怎么new出来的也不知道,由于所有的对象数据类型都是Object的一个实例,所以它也是Object的一个实例,也是Object的所属类;由于所有对象的_proto_都指向当前对象所属类的原型(prototype),所以Object的_proto_指向Object的原型prototype(指向了自己),指向自己没有意义(Object是对象类型的基类(最底层类),在它的原型上没有_proto_这个属性:因为即使有这个属性指向的也是自己,这个没有必要。其实准确来说是有的,只不过存储的值是null,我们可以理解为没有)

原型就是记住三句话画图。。。

16、原型链的查找机制

什么叫做原型链:跟作用域差不多,先看自己私有有没有,如果没有,通过自己的_proto_属性找公有,如果公有没有,一直向上查找,直到找到Object基类的原型为止

接15画出的图可以分析出

f1.say => 自己有,所以是私有方法

f1.eat => 自己没有(私有属性方法只有name、age、say),通过_proto_找公有的

f1.say===f2.say => flase(因为都是私有的)

f1.eat===f2.eat => true(私有都没有,找公有的)

f1.hasOwnProperty===f2.hasOwnProperty => true(私有都没有,通过_proto_找Fn.prototype下也没有,再通过Fn的_proto_找Object.prototype找到hasOwnProperty)

f1.proto.say === f2.say => false (一个公有,一个私有)

f2.eat = Fn.prototype.eat => true (都是公有的)

f1.constructor是Fn

Fn.prototype.constructor是Fn

Fn.prototype.constructor === Fn => true

f1是Fn的实例(因为f1通过new Fn出来的,所以是Fn的实例)也是Object的实例(因为f1是对象数据类型的,而且不管怎么找最终都能找到Object原型)

17、内置类原型链引发的一些底层问题思考

通过内置类的是怎么指向来了解原型链和js面向对象思想是怎么创建出来这些东西的。我们拿数组来举例

Object对象内置类,对象内置类里面有个Array类置类,所以说Array内置类是Object这个对象内置类的一个子类,那它们之间又是什么样的关系呢?如图:

附上图以及分析步骤:

=> 1、数组的内置类Array,类也是函数,所以有个自己的堆内存存储函数体中的代码字符串(内置的native code)

=> 2、还有一个内置类Object,类也是函数,所以有个自己的堆内存存储函数体中的代码字符串(内置的native code)

=> 3、所有的类天生自带prototype属性

=> 4、每个prototype都是对象数据类型的,浏览器会给它开辟一个堆内存:Array.prototype和Object.prototype

=> 5、在prototype原型上都会有constructor属性指向函数本身

=> 6、因为都是内置类,原型上都会有很多内置方法

=> 7、创建两个数组实例ary1和ary2,数组也是对象数据类型的,开辟一个堆内存,存储数据结构(私有属性,属性值)

=> 8、只要是对象数据类型就会有_proto_

=> 9、由于ary1和ary2都是Array的实例,所以它们的_proto_都指向Array.prototype

=> 10、Array不知道是怎么创建出来的,所以Array的_proto_指向Object.prototype

=> 11、所以我们平时用ary1.push()的意思就是:ary1首先通过原型链的查找机制,找到Array原型上的push方法,然后让push方法执行(执行push内置方法实现向数组末尾追加新内容)。

(push方法的本意就是向数组末尾追加,但是ary1.push()并不是说直接向数组末尾追加了,一步步走得话就是ary1首先通过原型链的查找机制,先找自己私有的,私有的没有push,再通过_proto_找到Array.prototype上的push)

=> 12、ary1.proto.push():直接跳过私有的通过_proto_找公有的,但是和ary1.push()有区别,ary1.push()执行的时候,push()方法中的this是ary1;ary1.proto.push()方法执行,push()方法中的this是ary1.proto

=> 13、Array.prototype.push() 也是让push方法执行

所有的xxx.xxx.xxx()的真正原理都是先通过面向对象当中提供的原型链的查找机制,找打某一个自己所属类原型上的方法,得到这个方法之后让方法执行,这是它的核心操作原理

=> 14、比如说写一个ary1.hasOwnProperty():先找私有的,前实例ary1中没有hasOwnProperty()这个方法;然后通过原型链_proto_找到Array.prototype公有的,也没有;然后再通过Array的原型链_proto_找到Object基类原型上的hasOwnProperty()方法,并且执行。

所有的对象或者所有的实例,之所以能够操作某些属性和方法的核心原理就是:是因为在它的原型链上能找到这些属性和方法,如果找不到则不能进行操作

通过以上这句话,就可以知道类数组和数组的区别是什么了。

类数组Arguments:结构和数组结构非常相似,但是它不是数组,那么它和数组的区别是什么?

=> 15、类数组arguments也是对象,有_proto_,类数组的_proto_直接指向Object.prototype的;

所以arguments不能用push()方法,因为arguments私有中没有push()方法,然后通过原型链_proto_找到Object.prototype,Object.prototype中也没有push()方法。

而数组先找Array.prototype,再找的Object.prototype,Array.prototype中有push()方法,所以数组能用push()方法。

所以数组和类数组的区别在于: 数组是Array的实例,类数组是Object的实例;虽然数组也是Object的实例,但它直属于Array,在数组这个实例的原型链上是能够找到push()这个方法的,而类数组直属于Object,Object不能找到push()方法,所以不能用这个方法。

结构虽然一样,但是由于两者的原型链导致类数组不能使用Array中的方法。

prototype叫原型,是用来存东西的地方;_proto_叫原型链,是通过它来查找;

所以ary1和arguments都能用hasOwnProperty()这个方法,因为她们都是对象,只要是对象,最终通过_proto_永远都能指向Object.prototype,所以都能用hasOwnProperty()方法(检测当前属性是否为当前对象的私有属性),比如:

=> 16、所以说,私有和公有是一个相对论,我们需要看相对于哪个对象而言:

  • 相对于实例来说push是公有的
  • 相对于Array.prototype来说,push就是自己私有的
  • 凡是通过_proto_找到的属性都是公有的,反之都是私有的

=> 17、通过arguments.proto=Array.prototype让arguments._proto_指向Array.prototype,这个Array.prototype也会指向到Object.prototype,所以说arguments最终也会指向到Object.prototype,只不过相当于在arguments._proto_直接指向Object.prototype的中间加了一层指向Array.prototype而已,所以现在arguments能使用Array.prototye中的方法(还有种方法是把类数组转换成数组,类数组就能使用Array.prototype中的方法)

IE浏览器屏蔽了我们使用或者修改_proto_(不兼容整个ie,移动端开发可以这样去使用)

18、原型链中的this问题

关于原型链中提供的私有(公有)方法中的this指向问题:

1、看点前面是谁,this就是谁

  • f1.say(); // this:f1

  • f1.proto.say(); // this:f1.proto

  • Fn.prototype.say(); // this:Fn.prototype

  • ...

2、把需要执行方法中的this进行替换

3、替换完成后,如果想要知道结果,只需要按照原型链的查找机制去查找即可

function Fn(name,age){
    this.name=name;
    this.age=age;
    this.say=function(){
        console.log('my name is'+this.name);
    }
}
Fn.prototype.say=function(){
    console.log('!i am'+this.age+'years old');
}
Fn.prototype.eat=function(){
    console.log('i love food~');
}
var f1=new Fn('张学波',19);
var f2=new Fn('王雪超',69);

// Fn是一个类,f1、f2是这个类的两个实例,
// f1.say === f2.say // false (两个都是私有)
// f1.eat === f2.eat // ture (两个都是公有)

// f1.say();//this:f1 查找私有的  'my name is'+f1.name:'my name is'+张学波
// f1._proto_.say();//查找的是公有的属性和方法  this:f1._proto_
// //'!i am'+f1._proto_.age (undefined) +'years old' 公有属性和方法没有age
// Fn.prototype.say();////'!i am'+f1._proto_.age (undefined) +'years old' 

19、在原型上批量扩展属性和方法

1、设置别名(小名)

function Fn(name,age){
    this.name = name;
    this.age = age;
}

// == 1、设置别名(小名)==
var pro = Fn.prototype; // => 指向同一个堆内存
pro.aa = function(){
    
};
pro.bb = function(){
    
};

var f = new Fn('xxx',28); 

2、重新构造原型(最常用)

function Fn(name,age){
    this.name = name;
    this.age = age;
}
// == 重新构造原型(最常用) ==
Fn.prototype.cc = function () {};
Fn.prototype = {
    //=>Fn.prototype让原型指向自己开辟的堆内存有一个问题:自己开辟的堆内存
    //中没有constructor这个属性,所以实例在调取constructor的时候找到的是Object,
    //这样不好,此时我们应该重新设置一下constructor,保证机制的完整性

    cconstructor:Fn,//重新设置constructor,保证机制的完整性
    aa: function () {},
    bb: function () {}
};


var f = new Fn('xxx',28); 
// f.cc => undefined:重新做原型指向后,之前在浏览器默认开辟的堆内存中存储的属性和方法都没有了,
// 只有在新内存中存储的才是有用的(可以把之前的遍历克隆到新的内存中,或者之前的原型上最好不要写属性和方法)


// // == 重新构造原型这种方式不适合内置类的原型扩展,内置类的原型不允许重定向空间,因为一旦重定向空间后,内置类原有的方法都没
// // 有了,整个js结构底层都乱了。内置类只能一个个去扩展属性和方法,不能批量扩展==
// Array.prototype = {};//=>内置类原型不允许我们进行重构,这样写不行
// Array.prototype.aa = 11;//=>只能像这样一个个去扩展方法和属性

jq怎么在它自定义类(内置类)上去批量扩展方法属性的

也是使用重新构造原型方法来实现的

~function () {
    var jQuery = function (selector,context) { // jQuery=>是一个方法也是一个类 传两个参数
        return new jQuery.fn.init(selector,context); // 
    };
    jQuery.fn = jQuery.prototype = {//jQuery是一个类,类上有原型jQuery.prototype,
        // 通过jQuery.fn = jQuery.prototype = {},jQuery.fn相当于就是jQuery的原型,而且通过 = {} 重构了,
        // 为了防止constructor修改,加上了constructor:jQuery,接下来里面写了个init()方法。所以当执行jQuery
        // 这个方法( var jQuery = function (selector,context))的时候,相当于new了一个init()方法,这个
        // 方法在它的原型上,
        constructor:jQuery,
        init:function (selector,context) {

        }
    };
    window.$ = window.jQuery = jQuery // 把jQuery方法挂载到全局上去
}();

重新构造原型方法实现在原型上批量扩展属性和方法的两个问题

1、constructor问题

2、原有方法会丢失

20、在内置类的原型上扩展方法

基于内置类的原型扩展方法

  • 我们新增加的方法最好设置一个前缀(这里我加的是my=>myDistinct):防止我们新增加的方法和内置的方法冲突,把内置方法替换掉了

用数组去重的列子来说明

function distinct(ary){
    var obj = {};
    for (var i = 0; i < ary.length; i++){
        var item = ary[i];
        if(typeof obj[item] !== 'undefined'){ 
 // typeof obj[item] !== 'undefined'说明item在obj中出现过,如果出现过,那在原有数组中也出现过,那么就删除
 // 删除可以用splice,但是splice删除后,后面每一项都需要向前提一位,如果后面有很多项,性能不好。所以我们
 // 用最后一项的值替换掉当前项(重复的) ary[i]=ary[ary.length-1];,当前项没了,再把最后一项删除掉 ary.length--/ary.pop();
            ary[i]=ary[ary.length-1];
            ary.length--;//ary.pop()
 // 但是把最后一项放在当前项了,下一轮再拿的时候还是会把当前项再重新拿一遍,所以我们要i--;
            i--;
            continue;

        }
        obj[item] = item;//把item当前对象作为属性名,把当前对象也作为他的属性值存储起来,
        //但是每次存之前需要判断这个对象里面已经有没有这个属性名了,如果有那就说明之前数组中
        //也出现过这一项,这一项重复了,我们需要把这一项删除掉。if(typeof obj[item] !== 'undefined')

    }
    obj = null;//obj只是一个临时存储,用完之后需要把这个堆内存释放掉
    return ary;//最后返回这个数组
}

var ary = [1,2,1,2,3,2,3,2,2];
console.log(distinct(ary));

但是数组内置方法不是 distinct(ary) 这样使用的;比如ary.sort()直接就可以使用,那我们也想像内置方法这样去使用, ary.distinct(),ary之所以可以直接调用sort()方法,是因为sort()方法在数组原型上,所以我们把distinct() 也扩展到数组原型上。=>

基于内置类的原型来扩展方法

这种扩展的一个好处就是,以后用这个方法的时候不是把方法执行传参了,而是直接让当前实例调这个方法执行

Array.prototype.myDistinct = function myDistinct() {//为了跟内置方法达到差不多的效果,尽量不传参,给函数增加myDistinct方法名
    //=>this:ary当前要处理的那个数组 这里把ary全部用this替换掉(直接用this来操作就可以了)
    var obj = {};
    for (var i = 0; i < this.length; i++){
        var item = this[i];
        if(typeof obj[item] !== 'undefined'){ 
            this[i]=this[this.length-1];
            this.length--;//ary.pop()
            i--;
            continue;

        }
        obj[item] = item;
    }
    obj = null;

}
var ary = [1,2,1,2,3,2,3,2,2];
ary.myDistinct();//现在就可以通过像内置方法一样去执行,这时this:ary
console.log(ary);//这里不再需要return,this就是要操作的数组,所有操作都把原有数组中的数据改了,所以直接输出原有数组就可以了

但是比如::数组排序再追加一个100

// 写法1:
ary.sort(function(a,b){
    return a - b;
})
ary.push(100);
console.log(ary);


// 写法2:链式写法 => 执行完成一个方法紧跟着就调取下一个方法
// 首先ary.sort()执行完返回的是一个排序后的新数组,排序后的新数组也是数组,属于Array这个类的一个实例,所以能接着调Array的方法
// 所以链式写法的原理是:执行完成一个方法后返回的结果依然是当前类的实例,这样就可以继续调取当前类的其它方法操作了
ary.sort(function(a,b){
    return a - b;
}).push(100);
console.log(ary);
//--------------------
ary.sort(function(a,b){
    return a - b;
}).push(100).pop();//Error in response to storage.get: TypeError: Cannot read property 'join' of undefined
//因为push()方法执行完后返回的是一个新增后数组的长度(这个长度是一个数字),数字没有pop()方法,数组Array才有
console.log(ary);
//--------------------
ary.sort(function(a,b){
    return a - b;
}).push(100).toFixed(2).substring(1).split('').reverse();
//ary.sort()执行完返回的是一个数组,数组可以继续调push()方法,push()方法执行完返回的是一个数字,数字可以继续调toFixed()方法
//toFixed()方法执行完返回的是字符串,字符串可以继续调substring()方法(看执行完返回的结果是什么类型的,可以调对应的方法)
console.log(ary);

实现链式写法

基于内置类的原型来扩展方法(myDistinct)通过返回的结果(return this:返回当前数组)最终实现链式写法

链式写法的原理是:执行完成一个方法后返回的结果依然是当前类的实例,这样就可以继续调取当前类的其它方法操作了

Array.prototype.myDistinct = function myDistinct() {//为了跟内置方法达到差不多的效果,这里尽量不传参
    //=>this:ary当前要处理的那个数组 这里把ary全部用this替换掉(直接用this来操作就可以了)
    var obj = {};
    for (var i = 0; i < this.length; i++){
        var item = this[i];
        if(typeof obj[item] !== 'undefined'){ 
            this[i]=this[this.length-1];
            this.length--;//ary.pop()
            i--;
            continue;

        }
        obj[item] = item;
    }
    obj = null;
    return this;
    //通过this把去重后的数组返回(为了实现链式写法,返回去重后的数组,这样执行完成这个方法后,我们还可以继续调取数组中的其它方法)
}
var ary = [1,2,1,2,3,2,3,2,2];
ary.myDistinct().sort();
//这里需要让ary.myDistinct()执行完的结果返回的是一个去重后的数组,把去重后的数组再执行sort()进行排序

链式写法需要我们知道每个方法执行完后返回的结果是什么,才知道下一步能继续调取什么方法

通过四个步骤:1、意义是什么;2、参数是什么;3、返回值是什么;4、原来数组是否改变,把数组的每个方法都记扎实(才能知道最后返回的数据类型是什么,才知道链式写法下一步能继续调取什么方法)

21、基于内置类原型扩展方法的面试题

实现 (3).plus(2).minus(1) =>4

// => 写法1(最优写法)----------------------------------------
//  1、给Number的内置类原型上增加两个方法plus()和minus()
//  2、(3).plus(2)执行plus()时,数字(3)是Number类的一个实例,所以可以直接调Number类原型上公用的
//     属性和方法(面向对象中的概念),plus()中的this就是要操作的这个数字(3),通过this写计算逻辑
//  3、返回数字(当前类的下一个实例)实现链式写法
//  4、通过Number(arguments[0]) || 0 判断传参没有,传的参数是不是数字
Number.prototype.plus = function plus() {
    var value = Number(arguments[0]) || 0;
    return this + value;
    // return this + arguments[0];//返回数字,才可以链式操作去调minus(1)
}
Number.prototype.minus = function minus() {
    var value = Number(arguments[0]) || 0;
    return this - value;
    // return this + arguments[0];//返回数字
}
console.log((3).plus(2).minus(1));

// => 写法2--------------------------------------------------
Number.prototype.plus = function plus() {
    if(arguments.length === 0) return this;
    if(typeof arguments[0] != 'number') return this;
    return this + arguments[0];
}

Number.prototype.minus = function minus() {
    if(arguments.length === 0) return this;
    if(typeof arguments[0] != 'number') return this;
    return this - arguments[0];
}
console.log((3).plus(2).minus(1));