11666

👣👣 JS的三大特性一直是比较难理解的内容,不少初学者甚至有一定经验的大佬都不一定能说的特别的清楚,更多的都是一知半解,而这部分内容又是JS最核心的内容。所以我最近总结了这篇JS封装篇,后面还会陆续发布JS继承篇。希望可以帮到大家。😄

希望点进来的盆友可以点个赞👍哦,你们的支持就是对我最大的鼓励💪!!!

好吧,让我们回到正题,

Let's go!💪💪

封装 把客观事物封装成抽象的类,隐藏属性和方法,仅对外公开接口。

举个栗子 🌰

封装,就是把一个零零散散的东西做成一个组件。

打个很简单的比方,有的人用电脑不需要机箱,主板内存条显卡电源都裸露在外面,他觉得这样挺好,可以散热。但是大部分人还是会用机箱把所有的硬件都包装起来。优点嘛,第一便于到处使用,第二对内部部件有一个完整性的包括,第三,把所有的东西封装起来,只留下若干个接口,usb,显示器,音响接口等等,让使用者更加便利,也让维护者更加清晰。

私有属性和方法 :只能在构造函数内访问不能被外部所访问(在构造函数内使用 var 声明的属性);

公有属性和方法(或实例方法): 对象外可以访问到对象内的属性和方法(在构造函数内使用 this 设置,或者设置在构造函数原型对象上比如 Person.prototype.xxx);

静态属性和方法: 定义在构造函数上的方法(比如 Person.xxx),不需要实例就可以调用;

ES6 前的封装

函数(function)--最简单的封装

前言: 首先第一个问题,函数是不是一种封装? 当然是封装了。《JavaScript 高级程序设计》一书中说了:

函数对任何语言来说都是一个核心的概念。通过函数可以封装任意多条语句,而且可以在任何地方、任何时候调用执行。

如何封装: 将零散的的语句写进函数的花括号内,成为函数体,然后就可以调用了。

未封装的代码:

var oDiv = document.getElementsByTagName("div")[0];
var p = document.createElement("p");
body.style.backgroundColor = "green";
p.innerText ="我是新增的标签内容"; oDiv.appendChild(p);

❎ 缺点:

  • 每次都会执行这段代码,造成浪费资源
  • 用以被同名变量覆盖掉——因为是在全局作用域下申明的,所以容易被同名变量覆盖
  • 封装后的代码:

    function createTag() {
      var oDiv = document.getElementsByTagName("div")[0];
      var p = document.createElement("p");
      body.style.backgroundColor = "green";
      p.innerText = "我是新增的标签内容";
      oDiv.appendChild(p);
    

    ✅ 优点:

  • 提高了代码的复用性;
  • 按需执行——解析器读到此处,函数并不会立即执行,只有当被调用的时候才会执行
  • 避免全局变量——因为存在函数作用的问题
  • 生成实例对象的原始模式

    假如我们把人看成一个对象,有名字和性别 2 个属性;

     var person = {
        name: '',
        sex: ''
    

    我们根据对象的属性,生成 2 个实例对象:

    var P1 = {};
    P1.name = 'Jack';
    P1.sex = 'boy';
    var P2 = {};
    P2.name = 'lisa';
    P2.sex = 'girl'
    

    好了,这就是最简单的封装了.

    有没有发现这样先有 2 大缺点:

  • 生成几个实例,写起来就非常麻烦,代码重复;而且还是相似的对象
  • 实例与原型之间没有什么联系;
  • 原始模式的改进——工厂模式

    function Person(name, sex) {
    return { name, sex }
    let p1 = new Person('Jack','boy');
    let p2 = new Person('lisa','girl');
    console.log(p1) //{name: "Jack", sex: "boy"}
    console.log(p2) //{name: "lisa", sex: "girl"}
    

    ✅ 优点: 解决代码重复的问题。

    ❎ 缺点: p1 和 p2 之间没有内在的联系,不能反映出它们是同一个原型对象的实例。

    构造函数模式

    所谓的"构造函数",其实就是一个普通函数,但是内部使用了 this 变量。对构造函数使用 new 运算符,就能生成实例,并且 this 变量会绑定在实例对象上。

    比如Person的原型对象这样写:

    function Person(name, sex) { this.name = name; this.sex = sex;

    现在可以生成实例了:

    let p1 = new Person('Jack','boy'); let p2 = new Person('lisa','girl'); console.log(p1) //{name: "Jack", sex: "boy"} console.log(p2) //{name: "lisa", sex: "girl"}

    constructor它是构造函数原型对象中的一个属性,正常情况下它指向的是原型对象 Person。

    这时 p1 和 p2 会自动含有一个constructor属性,指向它们的构造函数。

    alert(p1.constructor == Person); //true
    alert(p2.constructor == Person); //true
    

    instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型上。

    用法: object instanceof constructor

    object: 指某个实例对象;

    constructor:某个构造函数;

    instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

      alert(p1 instanceof Person); //true
      alert(p2 instanceof Person); //true
    

    是不是感觉构造函数模式还不错🤪

    其实构造函数模式依然存在一定的问题 ❓

    请继续往下看 👀

    构造函数模式存在的问题

    构造函数方法很好用,但是存在一个浪费内存的问题。 现在我们给Person对象添加一个属性age和一个方法getName,然后生成实例对象:

      function Person(name, sex) {
      this.name = name;
      this.sex = sex;
      this.age = 20;
      this.getName = function () { console.log('name') } }
      let p1 = new Person('Jack','boy');
      let p2 = new Person('lisa','girl');
      console.log(p1)
      console.log(p2)
    

    是不是没看出什么问题:😒 表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,age 属性和 getName()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率 ❎

    console.log(p1.getName == p2.getName) //false

    你现在是不是想问?(⊙ˍ⊙)?

    能不能让 age 属性和 getName()方法在内存中只生成一次 然后所有实例都指向那个内存地址呢?

    Of Course😄

    接着往下看 👇

    Prototype 模式

    Javascript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。 这意味着,我们可以把那些不变的属性和方法,直接定义在 prototype 对象上。

    function Person(name, sex) { this.name = name; this.sex = sex; Person.prototype.age = 20; Person.prototype.getName = function(){ console.log('name') let p1 = new Person('Jack','boy'); let p2 = new Person('lisa','girl'); console.log(p1) console.log(p2) p1.getName()

    这时所有实例的age和一个方法getName方法,其实都是同一个内存地址,指向 prototype 对象,因此就提高了运行效率。

    console.log(p1.getName == p2.getName) //true

    isPrototypeOf() 这个方法用来判断某个 proptotype 对象和某个实例之间的关系。

    alert(Person.prototype.isPrototypeOf(p1)); //true alert(Person.prototype.isPrototypeOf(p2)); //true

    hasOwnProperty() 每个实例对象都有一个 hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自 prototype 对象的属性。

    alert(p1.hasOwnProperty("name")); // true alert(p1.hasOwnProperty("age")); // false

    in 运算符 可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。

    alert("name" in p1); // true alert("age" in p1); // true //in运算符还可以用来遍历某个对象的所有属性。 or(var prop in p1) { alert("p1["+prop+"]="+p1[prop]); }

    一个完整的例子🎈:

    function Person(name, age) {
        //私有属性和方法
        var sex = '秘密㊙';
        var getSex = function () {
            console.log(sex)
        //公有属性和方法
        this.name = name;
        this.age = age;
        this.descript = '我是共有属性'
        this.getInfo = function () {
            getSex()
            console.log(this.name, this.age)
    //静态属性和方法
    Person.descript = '我喜欢吃火锅...';
    Person.work = function () {
        console.log('我是一名前端开发工程师')
    //给原型对象添加属性和方法
    Person.prototype.descript = '我是原型上的属性'
    Person.prototype.hobby = ['游泳', '跑步'];
    Person.prototype.getHobby = function () {
        console.log(p1.hobby)
    var p1 = new Person('丽萨',23);
    console.log(p1)
    console.log(p1.sex) //  私有属性  undefined
    console.log(p1.descript) // 我是共有属性
    //因为自己有descript属性,所以直接使用自己的属性
    console.log(p1.hobby)  // 原型中的属性  ["游泳", "跑步"]
    console.log(Person.descript)  //静态属性  我喜欢吃火锅...
    Person.work()  //静态方法    我是一名前端开发工程师
    p1.getInfo() //  共有方法  秘密㊙ 丽萨  23  
    p1.getHobby() //原型中的方法  打印hobby
    //自己没有该方法,但是在原型上找到了,所以可以打印出来
    // p1.getSex() // 私有属性   报错
    // p1.work(); // 静态方法  报错
    console.log(Person.sex) // 报错  静态属性
    

    一、私有属性、公有属性、静态属性概念:

  • 私有属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用 var 声明的属性);

  • 公有属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用 this 设置,或者设置在构造函数原型对象上比如 Person.prototype.xxx);

  • 静态属性和方法:定义在构造函数上的方法(比如 Person.xxx),不需要实例就可以调用;

    二、 实例对象上的属性和构造函数原型上的属性:

  • 定义在构造函数原型对象上的属性和方法虽然不能直接表现在实例对象上,但是实例对象却可以访问或者调用它们。

  • 当访问一个对象的属性 或者方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性或者方法或到达原型链的末尾(null)--(原型链查找);

    三、遍历实例对象属性的三种方法:

  • 使用 for...in...能获取到实例对象自身的属性和原型链上的属性;
  • 使用 Object.keys()和 Object.getOwnPropertyNames()只能获取实例对象自身的属性;
  • hasOwnProperty()方法传入属性名来判断一个属性是不是实例自身的属性;
  • 四、注意

  • this.xxx 表示的是给使用构造函数创建的实例上增加属性或方法,而不是给构造函数本身增加;
  • 只有Person.xxx 才是给构造函数上增加属性或者方法;
  • 如果自己有该属性或者方法,则用自己的属性或者方法,如果自己没有就一级一级的查找。
  • ES6之后的封装

    在ES6之后,新增了class 这个关键字。

    class就是类,和ES5中的构造函数十分相似,绝大部分功能都是一致的,但是class的写法,能让对象原型的功能更加清晰,更加符合面向对象语言的特点。

    与ES5封装写法的区别:

    ES5的写法:

     function Person (name, age){
         this.name = name;
         this.age = age;
     Person.prototype.getInfo = function(){
         console.log(this.name,this.age)
    

    ES6的写法:

    class Person{
         constructor(name, age){
            this.name = name;
            this.age = age;
         getInfo(){
            console.log(this.name,this.age)
    

    区别:

    class的内部所有定义的方法,都是不可枚举的。而原型上面定义的方法都是可以枚举的。

     function Person (name, age){
         this.name = name;
         this.age = age;
     Person.prototype.getInfo = function(){
         console.log(this.name,this.age)
    console.log( Object.keys(Person.prototype)) //["getInfo"]
    
    class Person{
         constructor(name, age){
            this.name = name;
            this.age = age;
         getInfo(){
            console.log(this.name,this.age)
    console.log( Object.keys(Person.prototype))  //[]
    

    注意:

  • constructor就是ES5构造函数的写法,而this关键字则代表实例对象。也就是ES5的构造函数Person对应ES6的Person类的构造方法constructor.
  • 定义类的时候前面不需要加function这个保留字,直接把函数定义放进去就ok了。
  • 方法之间无需加逗号,否则会报错;
  • 类的数据类型就是函数,类本身指向构造函数
  •  class Person{
         constructor(name, age){
            this.name = name;
            this.age = age;
    console.log(Person ===Person.prototype.constructor ) //true
    

    类的所有方法都定义在类的prototype属性上

     class Person{
         constructor(name, age){
            this.name = name;
            this.age = age;
         getName(){
            console.log(this.name)
         getAge(){
            console.log(this.age)
    //等同于
    class Person{
        constructor(name, age){
           this.name = name;
           this.age = age;
    Person.prototype= {
        getInfo(){
            console.log(this.name,this.age)
         getAge(){
            console.log(this.age)
    

    hasOwnProperty() javascript中提供了该方法,检测对象自身属性中是否含有指定的属性,返回一个布尔值。

    class Person{
        constructor(name, age){
           this.name = name;
           this.age = age;
    Person.prototype= {
        getInfo(){
            console.log(this.name,this.age)
         getAge(){
            console.log(this.age)
     var p = new Person('lisa', 23)
    console.log(p.hasOwnProperty('getInfo'))  //false
    console.log(p.hasOwnProperty('name'))  // true
    

    constructor方法

    constructor方法是类的默认方法,通过new命令生成对象实例时自动调用该方法。一个类必须有constructor方法,如果没有显示定义,会添加一个空的constructor方法。

    class Person {}
    //等同于
    class Person {
        constructor(){}
    

    注意:

  • constructor方法默认返回实例对象即this,不过可以指定返回另外一个对象;
  • 类必须用new来调用,否则报错。
  • 静态属性和静态方法(static)

    类相当于实例的原型,所有在类中定义的方法都会被实例继承。

    But🤷

    静态属性或者静态方法指的时class本身的属性或者方法,不是实例的属性或者方法,所以class可以直接调用。

    class Person{
        constructor(name, age){
           this.name = name;
           this.age = age;
        static descript = '我时一个静态属性';
        static getDescript(){
            console.log("我是一个静态方法")
     console.log(Person.descript) //我时一个静态属性
     Person.getDescript() //我是一个静态方法
    

    class的本质也是一个对象,所以也可以使用Person.xxx的方法定义静态属性和静态方法。

    class Person{
        constructor(name, age){
           this.name = name;
           this.age = age;
    Person.descript = '我时一个静态属性';
    Person.getDescript=function (){
        console.log("我是一个静态方法")
     console.log(Person.descript) //我时一个静态属性
     Person.getDescript() //我是一个静态方法
    

    类的实例属性

    在class中,使用=来定义一个属性和方法,效果与this定义相同,会被定义到实例上。

    class Person{
        constructor(name, age){
           this.name = name;
           this.age = age;
           this.getName = function () {
               console.log(this.name)
        myProp = '我是实例属性'; //实例的属性 
        getMyProp = function() { //实例的方法
            console.log(this.myProp)
        getInfo(){ //属于类的原型的方法
            console.log('获取信息')
    let  p = new Person('lisa', 23)
     console.log(p.myProp) //我是实例属性
     p.getMyProp() //我是实例属性
     console.log(p) 
     console.log(p.hasOwnProperty('getName'))  //true
     console.log(p.hasOwnProperty('getMyProp'))  //true
     console.log(p.hasOwnProperty('getInfo'))  //false
    

    this的指向

    类的方法内部如果含有this,它将默认指向类的实例。

    class Person{
        constructor(name, age){
           this.name = name;
           this.age = age;
           var type = '我是私有属性'
           this.desc = '我是通过this定义的属性'
           this.getName = function () {
               console.log(this.name)
        desc = '我是通过等于号定义的属性'; //实例的属性 
        getMyProp = function() { 
            console.log(this) //实例对象
            console.log(this.desc)  //我是通过this定义的属性
            console.log(desc) //我是外面定义的
    var desc = '我是外面定义的'
    let  p = new Person('lisa', 23)
    console.log(p)
    p.getMyProp()  
    

    解析:

  • 因为constructor自己有desc这个属性,所以实例优先使用该属性,如果constructor没有这个属性,则会使用=定义的属性。this默认指向了类的实例。

  • 打印desc这个变量时,发现自己没有该变量,所以一层一层往上找,直到找到了window中存在这个变量,才打印出该变量。

    不存在变量提升

    new Foo();
    class Foo{}
    

    上面的代码中,Foo类使用在前,定义在后,这样会报错。因为ES6不会把变量提升到代码头部。

    ES5存在变量提升,可以先使用后定义。

    var p = new Person()
    function Person () {}
    
  • class中如果存在同名的属性或者方法,用this定义的方法会覆盖用=定义的属性或方法;
  • 类的所有方法都定义在类的prototype属性上;
  • class不存在变量提升;
  • 静态属性或者静态方法指的时class本身的属性或者方法,不是实例的属性或者方法,所以class可以直接调用。
  • 类的方法内部如果含有this,它将默认指向类的实例。
  • 参考文档:

  • JavaScript面向对象编程

  • 阮一峰的 Javascript 面向对象编程

    希望看到这里朋友可以动动手点个赞👍哦,你们的支持就是对我最大的鼓励💪!!!

    本文使用 mdnice 排版

    分类:
    前端
    标签:
  •