一、原型规则

  1. 所有的引用类型(数组 对象 函数),都具有对象的特征,即可自由拓展属性(除了’null’ 以外);
  2. 所有的引用类型(数组 对象 函数),都有一个__proto__[隐式原型]属性,属性值是一个普通对象;
  3. 所有的函数,都有一个prorotype[显式原型]属性,属性值也是一个普通对象;
  4. 所有的引用类型(数组 对象 函数),__proto__属性值指向它的构造函数的'prototype’属性值;
  5. 当试图得到一个对象的某个属性时,如果该对象本身没有这个属性,那么会去它的__proto__属性即( 构造函数的prototype ) 中寻找, 由此形成了原型链

    var obj = {}; obj.a = 100;
    var arr = []; arr.a = 100;
    function fn(){};  fn.a = 100;
    
    console.log(obj.__proto__,arr.__proto__,fn.__proto__) 
    console.log(fn.prototype)
    console.log(obj.__proto__ === Object.prototype)//true
    
    ----------------------------------------------
    //属性查找
    //构造函数
    function Person(name, age){this.name = name;}
    Person.prototype.alertName = function(){
        alert(this.name)
    }
    
    //创建示例
    var f = new Person('jerry');
    f.printName = function(){alert(this.name)}
    
    f.printName();//子类自己补充的方法
    f.alertName();//调用通过prototype原型对象定义的alertName方法
    f.toString();//需要在f.__proto__.__proto__里面找
    
    f instanceof Person //true  用于判断 引用数据类型 属于哪个构造函数的方法
    f instanceof Object //true  再往上一层找 是属于Object 所以也是对的
    f.__proto__ === Person.prototype //true __proto__指向构造函数的prototype
    

二、实现继承方式

父类代码:

// 定义一个动物类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  this.data = [1];
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

一 、原型链继承

核心:将父类的实例作为子类的原型 

function Cat(){ 
}
Cat.prototype = new Animal();//主要代码 `子类的原型指向父类实例`
Cat.prototype.name = 'cat';

// 实例化一个猫
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true

// 再次实例化一个猫
var kitty = new Cat();
console.log(kitty.data);//[1];
cat.data.push(2)//实例一 往父级data属性添加
console.log(kitty.data);//[1,2]

特点:
1.非常纯粹的继承关系,实例是子类的实例,也是父类的实例
2.父类新增原型方法/原型属性,子类都能访问到
3.简单,易于实现

缺点:
1.要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
2.继承单一 子类原型只能指向一个父类,无法实现多继承
3.所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
4.无法向父类传参
总结:最大缺陷3、4

二 、借用构造函数继承

核心:用.call()和.apply()将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))

function Cat(name){
  Animal.call(this); //主要代码 `调用父类并把this指向子类`
  this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特点:
1.解决子类实例共享父类引用属性的问题
2.创建子类实例时,可以向父类传递参数
3.可以实现多继承(call多个父类对象)

缺点:
1.实例并不是父类的实例,只是子类的实例
2.只能继承父类的实例属性和方法,不能继承原型属性/方法
3.每个新实例都有父类构造函数的副本,臃肿,影响性能
总结:缺点3

三 、组合继承 (常用)

核心:组合原型链继承和构造函数继承

function Cat(name){
  Animal.call(this); //借用构造函数模式
  this.name = name || 'Tom';
}
Cat.prototype = new Animal(); //原型链继承

var cat = new Cat();
console.log(cat.name);  //tom; 子类属性 
console.log(cat.sleep()); //tom正在睡觉!; 继承构造函数方法 
console.log(cat.eat('饭'));//tom正在饭; 继承父类原型的属性
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

特点:
1.可以继承父类原型上的属性,可以传参,可复用。
2.每个新实例引入的构造函数属性是私有的。

缺点:
调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。

四、寄生组合继承 (常用)

核心: 通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

function Cat(name){
  Animal.call(this); //借用构造函数模式继承
  this.name = name || 'Tom';
}
//借用函数容器 砍掉父类的实例属性 输出对象和承载继承的原型
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();

var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

特点:
1.可以继承父类原型上的属性,可以传参,可复用。
2.每个新实例引入的构造函数属性是私有的。
3.避免了调用了两次父类构造函数

三、补充点Es6

class 开始定义类
constructor 构造函数
static 静态方法
class Child extends Parent 继承
super 传参给父类 或者调用父类的静态方法

//父类
class Parent{
    constructor(place = "china"){
      this.from=place;
    }
    //定义静态方法
    static tell(){ 
      console.log('tell'); //`不会被实例继承,而是直接通过类来调用 或者 从`super`对象上调用。 
    }
}
//继承
class Child extends Parent{
    //构造函数
    constructor(name,birth,age){
        this.args = ["hello","world"];
        this.name = name;
        this.age = age;
        
        super("USA"); //子类向父类传递参数
    }
    
    //取值函数(getter)和存值函数(setter)
    get age(){
        return this.age;
    }
    set age(val){
        this.age = val;
    }
    //原型方法方法
    goSchool(str){
        console.log(this.name+'迫不及待想要上学'+str);
    }
    
    //加* 方法是一个 Generator 函数
    * [Symbol.iterator]() {
        for (let arg of this.args) {
          yield arg; 
        }
     }
}

补充 call apply 与 bind

call(), apply() 和 bind()这三个函数都是用来完成函数调用, 并且设置this指向
call()和apply() 会立即调用函数 , apply第二个参数接受一个数组
bind() 不会立即调用, 而是返回了一个函数的拷贝。 另外还需要触发调用, 另一方面 它在拷贝的地方调用传入的参数也是会传给原始函数的

(1)call( )

var dist = 'Beijing';

function greet(name, hometown) {
  var word =  `Welcome ${name} from ${hometown} to ${this.dist}!`
  console.log(word);
}

var obj1 = {
  dist: 'Chengdu'
};

greet.call(obj1, "Tom", "Dallas");  // Welcome Tom from Dallas to Chengdu!

greet("Jerry", "Houston"); // Welcome Jerry from Houston to Beijing!

因为greet.call(obj)传入了obj1作为第一个参数,所以在 greet()函数执行时, this指向 obj1。其余的参数就将作为参数传给greet()函数。
当没有使用call()而直接调用greet()时, this指向 window对象.

(2)apply()

var dist = 'Beijing';

function greet(name, hometown) {
  var word =  `Welcome ${name} from ${hometown} to ${this.dist}!`
  console.log(word);
}

var obj1 = {
  dist: 'Chengdu'
};

var args = ["Tom", "Dallas"];
greet.apply(obj1, args);  // Welcome Tom from Dallas to Chengdu!

greet("Jerry", "Houston"); // Welcome Jerry from Houston to Beijing!

apply()函数和call()函数非常的相似,第一个参数都用来设置目标函数运行时的this指向。 唯一的区别就是 apply()的第二个参数接受一个数组, 其他表现则一样。

(3)bind( )

var dist = 'Beijing';

function greet(name, hometown) {
    var word = `Welcome ${name} from ${hometown} to ${this.dist}!`;
    console.log(word);
}

var obj1 = {
    dist: 'Chengdu',
};

var obj2 = {
    dist: 'Chongqing',
};

var greet1 = greet.bind(obj1, 'Tom', 'Dallas');
var greet2 = greet.bind(obj2, 'Spike', 'San Antonio');

greet('Jerry', 'Houston');

greet1();
setTimeout(function() {
    greet2();
}, 1000);

结果输出:
  Welcome Jerry from Houston to Beijing!
  Welcome Tom from Dallas to Chengdu!
  Welcome Spike from San Antonio to Chongqing!

结论:bind()函数同样完成了this会指向bind()的第一个参数。但并不会立即执行目标函数, 而是返回了一个函数的拷贝,其余传给bind()的参数都会按顺序传给返回的函数。我们就可以异步调用这个函数返回值了。
但是需要注意的是,bind()方法返回的函数拷贝在使用 new 操作时, 第一个参数是会被忽略的。

如果在调用返回的函数拷贝的时候, 又传入了新的参数, 会发生什么呢, 只有再写一个例子试试。

var obj1 = {
    dist: 'Chengdu',
};

function greet(name, hometown) {
    console.log(Array.prototype.slice.call(arguments));
    var word = `Welcome ${name} from ${hometown} to ${this.dist}!`;
    console.log(word);
}

var greet1 = greet.bind(obj1, 'Tom', 'Dallas');

greet1('Jerry', 'Houston');


输出结果:
[ "Tom", "Dallas", "Jerry", "Houston" ]
Welcome Tom from Dallas to Chengdu!

结论:两个地方传入的参数都会被传给目标函数,函数拷贝调用时传入的参数会追加在bind()函数调用时传入的参数后面
当然因为原函数只用到两个参数 所以在结果上就只显示了:Welcome Tom from Dallas to Chengdu!


参考资料:
https://www.cnblogs.com/humin...
https://www.cnblogs.com/ranyo...
https://www.jianshu.com/p/a00...
https://juejin.im/post/5c433e...


Jerry
481 声望203 粉丝

学习的付出 从不欺人。记忆总是苦,写总结最牢固