【JS】类、实例、原型 继承 call apply bind

164 阅读7分钟

复习总结

在面向对象的问题里面,很多东西都是相对而言的,比如把函数看成类还是看成对象来分析问题结果就是不一样的,比如是私有属性还是公有属性也是相对而言的。

而扰乱问题的点就在于函数,因为函数既能是类去构造实例,也可以是对象,是Function类构造出来的实例。

而自己定义的普通的函数作为类,构造出来的实例就只能是对象,没有其他的角色。

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

一些练习

  • 1
    function Fn() {
        let a = 1;
        this.a = a;
    }
    Fn.prototype.say = function () {
        this.a = 2;
    }
    Fn.prototype = new Fn;

    let f1 = new Fn;
  
    Fn.prototype.b = function () {
        this.a = 3;
    };

    console.log(f1.a);   // 1
    console.log(f1.prototype);  // undefined  

    console.log(f1.b);  
    //=>            function () {
    //                    this.a = 3;
    //                       };
    console.log(f1.hasOwnProperty('b'));// false
    console.log('b' in f1);  // true
    console.log(f1.constructor == Fn); // true
  • 2
   function fun(){
    this.a=0;
    this.b=function(){
        alert(this.a);
    }
   }
   fun.prototype={
    b:function(){
        this.a=20;
        alert(this.a);
    },
    c:function(){
        this.a=30;
        alert(this.a)
    }
   }
   var my_fun=new fun();
   my_fun.b();// "0"
   my_fun.c();// "30"
  • 3
    function C1(name) {
        if (name) {
            this.name = name;
        }
    }
    function C2(name) {
        this.name = name;
    }
    function C3(name) {
        this.name = name || 'join';
    }
    C1.prototype.name = 'Tom';
    C2.prototype.name = 'Tom';
    C3.prototype.name = 'Tom';
    alert((new C1().name) + (new C2().name) + (new C3().name));
// "Tomundefinedjoin"
  • 4
   function Fn(num) {
    this.x = this.y = num;
   }
   Fn.prototype = {
    x: 20,
    sum: function () {
        console.log(this.x + this.y);
    }
   };

   let f = new Fn(10);

   console.log(f.sum === Fn.prototype.sum);// true
   f.sum();// 20
   Fn.prototype.sum(); // NaN
   console.log(f.constructor); //Object

小技巧

可以用这种方法来检测当前实例是否是类的实例

function Fn(){

}
let f = new Fn;
console.log(f.constructor === Fn); // true

注意:有局限性,如果给当前实例增加了一个私有属性也叫constructor或者给类的原型重定向都有可能造成检测的结果不准确


类的继承

原型继承

function A(){
  this.getX = function(){}
}
A.prototype.getY = function(){}

function B(){}
B.prototype = new A;

let f = new B;
f.getX()  //可以继承类A实例的私有方法
f.getY()  //可以继承类A实例的公有方法

解析: 让类B的原型指向类A的实例,那么以后类B的实例不仅可以继承类A实例的私有方法,还可以继承类A实例的公有方法。这种继承叫做原型继承。相当于是将类B的原型重定向,使类A的实例的私有属性方法和公有属性方法都变成了类B的实例的公有属性方法。

但是需要注意,这种继承方式抛弃了类原有的原型,原有的原型上的公共方法就没了;另外,只能改变自己自定义的类的原型,内置的类的原型是无法重定向的。

中间类继承

function fn(){

arguments.__proto__ = Array.prototype;
arguments.push(500);
console.log(arguments);
}

fn(100,200,300,400)

//=> [100,200,300,400,500]

解析:

本例中。arguments虽然不是Array的实例,但是我们可以手动把arguments的____proto____ 指向Array的原型,这样arguments就可以使用Array原型上的方法,这种继承就叫做中间类继承。

注意:

这种方法不能随便使用,字面量形式创建的基本数据类型是无法改变____proto____ 的指向的,引用数据类型也不可以随便改变,不然得到的结果会乱七八糟的。本案例中,因为arguments的类数组,与数组的形式类似,所以改变指向为数组类的原型以后,可以正常调用数组的公共方法。毕竟每个类的原型的公共方法都是符合自己类的形式的方法,内部的代码都是与自己的形式相关的,所以不要随便使用这种继承方式调用其他类的方法。


toString.call()

是Object原型上的方法,用来检测数据类型

console.log(Object.prototype.toString.call(1))
//=>  "[object Number]"
console.log(Object.prototype.toString.call("1"))
//=>  "[object String]"
console.log(Object.prototype.toString.call(true))
//=>  "[object Boolean]"
console.log(Object.prototype.toString.call(null))
//=>  "[object Null]"
console.log(Object.prototype.toString.call(undefined))
//=>  "[object Undefined]"
console.log(Object.prototype.toString.call([]))
//=>  "[object Array]"
console.log(Object.prototype.toString.call({}))
//=>  "[object Object]"
console.log(Object.prototype.toString.call(Array))
//=>  "[object Function]"

注意: toString.call()不能检测自定义类的实例,如果检测自定义类的实例就返回的都是"[object Object]"

function Fn(){}
console.log(Object.prototype.toString.call(Fn))
//=>  "[object Function]"

let f1 = new Fn;
console.log(Object.prototype.toString.call(f1))
//=>"[object Object]"

改变函数中this指向的方法

     /* 
        改变函数this指向的方法
         */
         let obj = {};
         function fn(){
             console.log(this); // window  obj
         }
        //  fn();

         obj.fn = fn;
         obj.fn();
         delete obj.fn;
         
         // 我想让fn函数执行时,this指向当前obj对象

这样虽然可以实现改变this的效果,但是太麻烦,每次执行都要进行一遍这几条代码。


函数中的this是不允许直接修改的

let obj = {name:1};
function fn(){
this = obj;
console.log(this);
}
fn();
//=> Uncaught ReferenceError: Invalid left-hand side in assignment

注意: this不能单独出现在等号的左边进行赋值!

call、apply、bind就是内置的改变函数中this指向的三种方法,直接调用即可。

call、apply、bind这三个方法存在于Function类的原型上,所有的函数都可以进行调用(因为所有的函数都是Function类的实例)。

call方法

是Function类的原型上的方法。

call是用来改变执行函数中的this指向的,将要执行的函数中的this改变为call方法传进来的参数,并且改变完以后让函数立即执行。

比如toString.call(),实际上执行的是call方法,但是call方法里面的this变为了toSring,而toString中的this变为传进来的参数,传的是谁,this就是谁。最后就相当于还是执行的toString方法,只不过将里面的this改变为了传进来的参数,然后立即执行了。

let obj = {name:1};
function fn(){
console.log(this)
}

fn.call(obj)
//=> {name:1}

//把fn当做对象找到当前所属类的原型上的call方法,这句话是让call执行

解析: 执行过程:

1.fn通过____proto____ 找到当前所属类的原型(Function类的原型)上的call方法

2.在call的内部把call函数里面的this指向了fn,让call方法执行,也就是call里面的this执行,也就是fn执行,并且给call传递实参

3.在call的内部,让fn执行了,并且把fn的this指向改成了你传递的第一个实参

4.表面上看执行的是call方法,实际上是执行的fn方法,并且把fn的this给改变了


总结: fn.call(参数1,参数2,...)

1.在严格模式下,如果call不传参,或者传undefined,fn的this指向都是undefined,如果传null,this指向null

"use strict"
let obj = {name:1};
function fn(){
console.log(this)
}

fn.call()

//=> undefined
"use strict"
let obj = {name:1};
function fn(){
console.log(this)
}

fn.call(null)

//=> null

2.在非严格模式下,如果call不传参,或者传undefined,或者传null,this都是指向 window

let obj = {name:1};
function fn(){
console.log(this)
}

fn.call(null)

//=> Window

3.call的第一个参数是fn的this指向,从第二个参数开始都是fn的正常参数

apply方法

它和call方法是一样的,都是改变this的指向,但是不同点是传参方式不一样,apply的第二个参数是一个数组或者类数组,相当于把数组或类数组中的每一项作为参数一一对应的传递给前面的函数的形参。

let obj = {name:1};
function fn(n,m){
console.log(this,n,m)
}

fn.apply(obj,[1,2])

//=> 这里相当于是给参数 n 传了数组的第一项 1 ,参数 m 传了数组的第二项 2

bind方法

这个方法也是改变this指向的,但是他是预处理this,他会提前改变函数的this指向,并不会让fn函数立即执行,bind方法的返回值才是改变this之后的函数。需要再加上() 才是执行,不然得到的就是改变完this的新函数。

let obj = {name:1};
function fn(n,m){
console.log(this,n,m)
}

let res = fn.bind(obj);
res();
//=>{name:1}

console.log(res === fn);
//=>false

res();
//=>{name:1}

这就说明bind改完this指向以后返回的函数可以多次执行。而原函数并没有发生改变,返回的函数是一个新的函数,存放在一个新的堆内存里面。

传参的方法有两种

//第一种:
let res = fn.bind(obj,1,2);

//第二种:
let res = fn.bind(obj)
res(1,2)

一种是在bind执行时给函数传参,另一种是在bind返回的函数执行时给函数传参

小案例

let ary = [100,200];
console.log(ary.slice(0));//克隆数组
//=> [100,200]
//slice方法里的this是谁
//谁调用的slice,那slice内部的this就是谁
function fn(){
//如何arguments.slice

console.log([].slice.call(arguments,0))

//通过[]找到slice方法,然后再用call方法让slice执行,并且把slice的this指向arguments,那么这样slice内部操作this时就是操作的arguments了
//slice内部的this是arguments
}
fn(100,200,300,400);
//=> [100,200,300,400]

而且这样返回的数组,可以实现把arguments这个类数组转化为数组。arguments本身没有变化,相当于是克隆出来了一个数组。

function fn(){

[].push.call(arguments,500);
console.log(arguments);
}
fn(100,200,300,400);

相当于arguments调用了push方法,这样arguments还是类数组,并且给末尾增加了一项。