【JS】函数的三种角色,原型链,原型重定向,属性检测

361 阅读8分钟

复习和总结

总结

分析一个问题之前,先要想清楚,它是函数还是对象。是函数就有prototype属性;是对象就有____proto____ 属性,就能走原型链机制,原型链机制查找的都是所属类的原型,一级一级向上找,直到Object类的原型。

1.所有的实例都是对象。

所有的对象都是实例。

所有的类都是函数。

2.构造函数模式解决的是实例的私有属性的问题。

原型模式解决的是实例的公有属性的问题。

3.构造函数里的this就是当前实例。

原型上的方法里的this一般情况下是当前类的实例,因为一般是实例调用的。

4.可以给类的原型上扩展公共方法,供实例调用。

注意: 给类的原型拓展方法的时候不要和内置的方法重名,为避免重名可以在起名字的时候加一些前缀,比如加个 my ...

为了让扩展的方法可以被链式调用,可以return的时候返回处理完的实例,然后就可以继续调用其他方法

注意: 如果想要实现链式调用,必须保证当前方法的返回值是当前类的实例,例如数组的公共方法中,有push方法,因为push方法的返回值是数组的length ,所以调用push方法后面无法链式调用

原型链机制

1.每一个函数都天生自带一个prototype属性,其属性值是一个对象,里面存储的就是当前类的实例的公有的属性和方法。(原型)

2.每一个原型天生自带一个constructor属性,其属性值指向当前的类

3.每一个对象天生自带一个 proto 属性,其属性值指向当前所属的类的原型

原型链的具体情况

1.如果一个的实例的所属类不知道是谁,那就把它的 proto 指向当前所属类的原型(所有原型的 proto 都指向内置类 Object 类的原型)

2.Object类的原型的 proto 应该指向自己,但是js认为自己指向自己没有意义,就规定为了 null

3.获取对象里一个属性名对应的属性值,先看自己私有的有没有,如果有就拿来用,如果没有就通过 proto 找到当前所属类的原型,如果原型上有就直接使用,如果原型上也没有,就通过原型的 proto 找到原型的所属类的原型,直到找到Object类的原型为止,如果还没有,就是undefined

练习题

      function Fn(x, y) {
            this.x = x;
            this.y = y;
            this.getX = function() {
                console.log(this.x);
            }
        }
        Fn.prototype.getX = function() {
            return this
        }
        Fn.prototype.getY = function() {
            console.log(this.z); // 
        };

        var f1 = new Fn(300, 200);
        var f2 = new Fn(100, 200);
        f1.__proto__.x = 300;
        f1.__proto__.__proto__.z = 315;
        f2.r = 300;

        f1.x = "恭喜发财";
        f1.__proto__.eat = function() {
            return this.z;
        };


        console.log(Fn.prototype.getX().x === f2.r);   //=>  true
        console.log(f1.z);   //=> 315
        console.log(Fn.prototype.__proto__.__proto__);  //=>  null
        console.log(f2.eat());   //=> 315
        console.log(Fn.prototype.getY());  //=> 315 undefined
        f1.getX() //=>  "恭喜发财"

原型重定向

练习题

  function Fn() {
      this.x = 100;
      this.y = 100;
    };
    Fn.prototype.getX = function () {
      console.log(this.x);
    };
    let f1 = new Fn;

    Fn.prototype = {
      getY: function () {
        console.log(this.y);
      }
    };
    let f2 = new Fn;
    console.log(f1.getX); 
    console.log(f2.getX);
    console.log(f1.constructor);
    console.log(f2.constructor);
    console.log(Fn.prototype);

分析: 本题中,先构造了一个实例,然后重定向了,而且重定向的是一个新的堆内存,然后又构造了一个实例。 先构造的实例是以默认开辟的原型为所属类的原型的,所以原来的那个原型不销毁。 后构造的实例是以重定向以后的堆内存为所属类的原型。

注意

1.注意分析重定向以后,原来的原型是否销毁,被占用就不销毁

2.原型是否有constructor属性,手动重定向的原型是没有constructor属性的,没有的话可以手动添加,也可以通过原型链机制找到所属类的原型里面的constructor属性

3.构造的实例的 proto 是指向哪个原型,重定向之前构造的实例是指向默认形成的原型的,重定向以后构造的实例是指向重定向以后的原型的

4.重定向的原型的____proto____ 通过原型链机制指向哪里,是Object类的原型还是原来的原型,看具体情况而定。手动重定向的堆内存如果不知道____proto____ 通过原型链机制指向哪里,那么就指向Object类的原型。

5.内置类的原型不允许重定向,但是可以往原型里面新增方法

属性检测

注意: 属性分为私有属性和公有属性,而是私有属性还是公有属性是看相对于谁来说的,自己有的就叫私有属性,需要通过原型链去所属类的原型上查找调用的属性就是公有属性。

只要是通过原型链机制找到的属性就是公有属性,更简单来说就是自己没有,然后还找到了这个属性,那就是公有属性。

检测是否是属性

in可以检测一个属性是否是实例中或者可以通过原型链机制查找的到的属性,只有这个属性,不管是私有属性还是公有属性,在原型链上能找得到的就返回true,否则就返回false

let ary = [1,2,3,4];
console.log("push" in ary);
console.log("push" in Array.prototype);

//=>  true  true

检测私有属性

hasOwnProterty 是检测当前属性是否是私有属性的,如果是私有属性就返回true,如果是公有属性或者没有这个属性就返回false。

let ary = [1,2,3,4];
console.log(ary.hasOwnProperty("push"));
console.log(Array.prototype.hasOwnProperty("push"));

//=> false  true

分析: 本例中检测push方法,是否是私有属性,push方法在Array这个类的原型上,相对于数组实例来说,是数组实例的公有属性,但相对于Array的原型来说就是其私有属性。

封装一个检测公有属性的方法

this => 当前调用方法的实例

先检测一下当前传递进来的属性名是否符合规则(字符串 数字)

用in检测一下当前的value是否是实例的属性,如果是再继续往下检测,如果不是就直接返回false

function hasPubProperty(value){
  if(typeof value === "string"||"number"){
   let n = value in this;
  //如果返回true,说明value是当前实例的属性
  let m = this.hasOwnProperty(value);
  if(n === true && m === false){
    return true;
  }
  return false;
  }

}

Object.prototype.hasPubProperty = hasPubProperty;

let ary = [100,200,300,400];
console.log(ary.hasPubProperty("push"));
console.log(ary.hasPubProperty(0));
console.log(ary.hasPubProperty("o"));


//=> true  false  false

注意: 本方法有局限性,如果一个属性在实例中有,在实例所属类的原型中也有的话,那它既是私有属性又是公有属性,这种情况下,会按私有属性处理,检测不出来公有属性,目前这个问题还需要研究解决一下

函数的三种角色

1.函数数据类型:普通函数、构造函数(类)

2.对象数据类型:普通对象、数组、Math、实例对象、arguments、正则对象、原型、元素集合、元素、函数

函数也是对象!!

函数里面有prototype这个属性,有键值对了,所以早就可以看出来函数是对象了

函数作为对象还有很多属性,比如有length属性、有name属性等等

函数作为对象时:

1.当函数作为对象的时候,每个函数都具有____proto____ 属性

2.Function 类是所有函数的基类,也就是说所有函数都是Function 类的实例

3.Function 类自己也是函数,只要是函数,就是Function 类的原型,所以Function 类通过 proto 指向自己的原型

4.Function 类的原型是一个匿名空函数,可以执行,但是没有返回结果,浏览器默认返回undefined,什么都不输出

Function.prototype === Function.__proto__

//=> true

总而言之就是,所有函数都是Function类的实例,而所有对象最终都通过原型链指向Object类的原型。Function类 和 Object类是两大基类

这么来看就是只要把方法写在Object的原型上,就总能被找到

小插曲: Object的原型就没法通过原型链继续往下找了,而且只要把方法写在Object的原型上,就总能被找到,所以个人感觉Object的原型是最大的。不过争论谁最大也没有什么意义,就好像讨论先有的鸡还是先有的蛋,没有结果。

一道经典面试题

 function Foo() {
            getName = function () {
                console.log(1);
            };
            return this;
          };
Foo.getName = function () {
            console.log(2);
        };
Foo.prototype.getName = function () {
            console.log(3);
        };
var getName = function () {
            console.log(4);
        };

function getName() {
            console.log(5);
        }

        Foo.getName(); //=> 2  Foo作为对象
        
        getName();  //=> 4  getName是个全局变量,存储的函数执行
        
        Foo().getName(); // Foo先执行,全局变量getName之前存储的函数被覆盖,然后在用返回值找到全局变量getName,将函数执行   //=> 1
        
        getName(); //=> 1
        
        new Foo.getName(); // 先执行 Foo.getName,Foo作为对象,假设找到的结果是af0 然后执行 new af0()  //=> 2           
        new Foo().getName();//=>有参数列表执行,作为类,构造函数执行,创建一个实例,通过原型链机制找到公有属性getName执行 //=> 3
        
        new new Foo().getName();//先new有参数列表执行,然后找到公有属性,然后再构造函数执行 //=> 3

        /*  JS的运算符优先级
        1、成员访问:寻找对象里的属性名所对应的属性值就是成员访问(19)
            Fn.aa
        2、new(带参数列表):就是构造函数执行有括号(19)
        3、new(无参数列表):就是构造函数执行没有括号(18)
        优先级一样,从左到右运算
         */