一篇通俗易懂的关于“原型” & “this”的解释

共 13270字,需浏览 27分钟

 ·

2021-10-22 03:25

转自:掘金 - i.m.t  https://juejin.cn/post/6921686794987634695

主题

今天想跟大家分享一个比较 "别扭" 的概念:“原型 & this” 。

想把这玩意儿给说清楚,大多都会感到头大。用的时候也会遇到些尴尬的场景。就很难去整明白,这到底是个啥。

这一期,就试着将这 说个清楚,讲个明白。开始~

原型

什么是 原型 ?带着这个问题往下看。

原型-构造器 (constructor)

首先说到原型,那就跟对象密不可分。如果我们需要创建一个对象,就需要区定义一个object。那我们在开发中如何去创建一个对象?肯定有人会说,就是var 一个对象呗。很好你说的很对~ 确实是var 一个对象,那我如果需要两个呢?这个时候又会说了,那就var两个呗。很好,你又说对了~

以下是创建对象的方法。

code 创建对象

var zhangsan = {
 name:'张三'
    age:20
}

var lisi = {
 name:'李四'
    age:22
}


那如果我们需要创建100个对象呢?程序员这么懒,不会去实打实的真的给你去 var 100个对象。当然如果真去这样做了,里面的变量也是未知的。何况如果是一个动态创建的,也不能去给代码写死不是。

好了,那这个时候,聪明的同学就已经想到了,搞一个 function 函数呗。专门生成对象,不就完事拉!

code 创建对象

function User(name, age) {
    var person = {} // 定义一个person 对象
    person.name = name; // 往对象中绑定传参
    person.age = age;
    return person // 返回生成的新对象
}

var zhangsan = User('张三', 20);
var lisi = User('李四', 22);


以上的函数,就会生成你想要的任何对象,也称之为:工厂函数 !一个专门造对象的工厂函数。

好了,那么这样做就可以了吗?是不是发现了什么?

对拉,js中,本身就有一种生产对象的方式啊,并且更简单,不需要再函数中定义一个对象。只需要绑定 this 就可以了。

code 创建对象

function User(name, age) {
  this.name = name; // 这里面的this,就代表了即将生成的那个对象 ,并且绑定传参
  this.age = age;
}

var zhangsan = new User('张三', 20);
var lisi = new User('李四', 22);


这个时候,细心的同学已经发现了不同之处。两个都是生成对象的函数,但是叫法就有些不同了。如果是用第二种 js 本身的函数,我们就需要用 new 关键字来生成对象。

code 差异

var zhangsan =  User('张三', 20); //  第一种
var zhangsan = new User('张三', 20); //  第二种


而这种需要用 new 关键字来叫的函数,称之为:“构造器 constructor or 构造函数”

而生成对象的这个过程,称之为:实例化“zhangsan” 可以称之为一个对象,也可以称之为一个 实例

原型-proto & prototype

好了,上一段说了构造器,那么构造器是干嘛的?就是造对象的一个函数呀。

那这一段,来说说原型中的重头戏。先看一段代码:

code 创建对象 在对象中添加一个功能属性,可以引用自己的属性 "greet"

function User(name, age) {
  this.name = name; // 这里面的this,就代表了即将生成的那个对象 ,并且绑定传参
  this.age = age;
  this.greet = function () {
    console.log('你好, 我是' + this.name + ',我' + this.age + '岁');
  }
}

var zhangsan = new User('张三', 20);
var lisi = new User('李四', 22);

zhangsan.greet() // 你好我是张三,我20岁
lisi.greet() // 你好我是李四,我22岁

这个时候,用生成的对象来叫一下 greet 这个方法,一点毛病没有。但是有没有同学发现什么问题?细心的同学已经发现了,这两个都分别实例了greet !

是不是有的同学有点没理解这句话的意思?没关系,接着看:

code 实例化后引用 greet 差异对比

zhangsan.greet === lisi.greet  // false

同学们,看到了什么?what? 这两个不一样?

这意味着什么呢?也就是说 张三 和 李四,实例化之后,都在自己的内部,创造了 greet 这样的属性。

这个时候,greet 的功能都是一模一样的呀。如果实例100个对象,岂不是要拷100份?完全没必要呀。有没有什么方法将这些通用的属性,放到一个地方呢?

有的。接下来就要说到本段的重头戏之一:prototype 了。在讲之前,先看下面一段代码:

code 创建对象 自带 prototype

function test1 () {}
console.log( test1.prototype ) // { constructor : f }

function test2 () {}
console.log( test2.prototype ) // { constructor : f }


发现了什么?是不是每创建一个function,都会自带一个 prototype 这样的对象啊。这就是js 的原生机制。那为什么 js 的原生机制 要这么做呢?划重点:prototype 就是给他即将生成的对象,继承下去的属性 看到了什么?prototype 他是一个属性,是一个可供实例对象继承下去的属性。这不简单了吗。走一个。

code 创建对象 在对象中添加一个功能属性,可以引用自己的属性 "greet"

function User(name, age) {
  this.name = name; // 这里面的this,就代表了即将生成的那个对象 ,并且绑定传参
  this.age = age;
}
User.prototype.greet = function () {
  console.log('你好, 我是' + this.name + ',我' + this.age + '岁');
}

var zhangsan = new User('张三', 20);
var lisi = new User('李四', 22);

zhangsan.greet() === lisi.greet() // true


既然知道了在构造函数中,使用 prototype 这样的继承对象,可以将 通用 的属性给 实例化的对象继承下去。

那么说到这,是不是会有几个问题?这个greet 并不是定义在实例化的对象里面的啊,来看一段代码:

code prototype

function User(name, age) {
  this.name = name; // 这里面的this,就代表了即将生成的那个对象 ,并且绑定传参
  this.age = age;
}
User.prototype.greet = function () {
  console.log('你好, 我是' + this.name + ',我' + this.age + '岁');
}
var lisi = new User('李四', 22);
console.log(lisi);
  /*
  User {}
  name:'李四'
  age = 22
  __proto__
  greet:f()
  constructor : f User (name, age)
  __proto__:Object
  ...
  */          

看到了什么?是不是通过 prototype 定义的_greet = function ()_ 属性跑到了 proto 下面去了。并且,这个greet属性虽然没有在自己本身的对象下面,但是一样可以使用啊!我们上面说到过:prototype 是继承属性对象。那么看到这里的小伙伴,是不是会困惑,为什么继承属性会定义在 proto 下面?先别急。接着看!

这个时候已经看到了重头戏之二:proto。再来看一段代码:

code __proto__

function Test () {}
Test.prototype.name = 'test'
var test01 = new Test()
var test02 = new Test()
test01.__proto__ === test02.__proto__    // true
// ----------------------- 实例之后的对象调用__proto__指针指向的 等于被实例的构造函数的prototype!
// test01.__proto__ = Test.prototype  // true


这时候,是不是已经恍然大悟了!原来通过prototype 定义的属性,再被多个实例化之后,引用的地址是同一个!并且 proto 就是我们上面使用的prototype 属性的马甲啊!就是说,我们在构造函数中使用prototype 定义的属性,都会被 proto 指针引用!

好了,这个时候,可以整一段比较晦涩的总结了:每个对象都有一个 proto 的属性,指向该对象的原型。实例后通过对 proto 属性的访问 去对 prototype对象进行访问;原型链是由原型对象组成的,每个对象都有__proto__属性,指向创建该对象的构造函数的原型 ,然后通过__proto__属性将对象链接起来,组成一个原型链,用来实现继承和共享属性!

理清楚以上关系后,可以想一下 通过prototype 定义的属性作用就仅仅如此么?接着看一段代码:

code prototype

function Test () {}
Test.prototype.name = 'test'
var test01 = new Test()
console.log( test01.name ) // "test"
Test.prototype.name = 'no test '
console.log( test01.name ) // "no test"


看到了什么?原来 prototype 可以在实例之后,再进行更改呀!

就是说,通过构造函数去改变name 的值,实例化之后的对象,引用的属性值也会跟着变。太强大了!

再来看看 constructor :

code constructor

function User(name, age) {
  this.name = name; // 这里面的this,就代表了即将生成的那个对象 ,并且绑定传参
  this.age = age;
}
User.prototype.greet = function () {
  console.log('你好, 我是' + this.name + ',我' + this.age + '岁');
}
var lisi = new User('李四', 22);

// 再次构造
var zhangsan = new lisi.constructor('张三', 20) // 使用constructor来实例化!!!
new lisi.constructor() === new User()  // true
console.log(zhangsan)
/*
  User {}
  name:'张三'
  age = 20
  __proto__
  greet:f()
  constructor : f User (name, age)
  __proto__:Object
  ...
  */  


发现了吗?就算我只能知道实例后的对象,但是我可以通过 proto 去找到这个实例对象的构造函数 constructor ,我再通过这个构造函数再去实例对象。(var zhangsan = new lisi.constructor('张三', 20))与我直接var zhangsan = new User('张三', 20)。完全一样。真的很强大!

好了,讲到这,proto & prototype 也就说完了,接下来再说说 原生对象的原型

原型-原生对象的原型

前面,知道了原型的概念,那就趁热打铁,接着看看原生对象的原型。

先看一段代码:

code 原生对象

  var a ={}
  console.log(a)
  /*
    {}
    __proto__
    greet:f()
    constructor : f Object()
    ...
    */  


可以看到,我们var 了一个新对象之后,没有定义任何属性,但是也能看到他的构造函数:Object()。也就是说:var a ={} === var a = new Object(),两者没有任何区别。举个例子:

code 原生对象

  var a ={}
  var b = new Object()
  console.log(a.constructor === b.constructor ) // true


可以看到,构造函数完全一样。

那么这个时候,可能会有同学想问,怎么去创造一个干净的对象呢?里面没有任何集成的属性等。

当然也是可以的。接着看:

code 原生对象

  var a = new Object.create(null) // 创建函数必须传参,一个对象或者是 null ,否则会报错!
  console.log( a )
  /*
    no prototies 
    */  


可以看到,通过 Object.create() 创建的对象,属性为空。这个时候,肯定会有同学有疑问,你这传的参数是 null,那当然什么都没有了,你传个对象试试。哈哈哈,确实,如果传对象的话,那就是定义自己所自带的原型了。举个例子:

code 原生对象

  var a = new Object.create({name:juejin,des:"666"}) // 创建函数必须传参,一个对象或者是 null ,否则会报错!
  console.log( a )
  /*
    {}
    __proto__
    name:juejin
    des:"666"
      __proto__
      constructor : f Object()
      ...
    */   


可以看到,再Object.create() 中传入对象的属性,是放在第一层的 proto 下面的,也就是中,这是你创建的这个原型对象的继承属性,意味着,可以根据自身的业务需求,来定义自己的原型对象!

多级继承链

好了,上面已经详细的讲解了原型链,构造函数,那么就试着来实现一个继承链。看下面代码:

code 继承链 从祖父 到爷爷 到爸爸 到自己

// Animal --> Mammal --> Person --> me
// Animal 
function Animal(color, weight) {
  this.color = color;
  this.weight = weight;
}
Animal.prototype.eat = function () {
  console.log('吃饭');
}

Animal.prototype.sleep = function () {
  console.log('睡觉');
}
 //  Mammal
function Mammal(color, weight) {
  Animal.call(this, color, weight); //绑定 this 这个下面讲
}

Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.suckle = function () {
  console.log('喝牛奶');
}
//  Person
function Person(color, weight) {
  Mammal.call(this, color, weight);
}

Person.prototype = Object.create(Mammal.prototype);
Person.prototype.constructor = Person;
Person.prototype.lie = function () {
  console.log('你是个骗子');
}
// 实例
var zhangsan = new Person('brown', 100);
var lisi = new Person('brown', 80);
console.log('zhangsan:', zhangsan);
console.log('lisi:', lisi);  
    


上面的代码中,实现了三级继承。其中,使用了我们上面讲到的 prototype 以及 Object.create() 。

code

function Animal(color, weight) {
  this.color = color;
  this.weight = weight;
}
Animal.prototype.eat = function () {
  console.log('吃饭');
}


往祖父类中写入继承属性,eat 供爷爷辈来继承这个吃的属性。

code

//  Mammal
  function Mammal(color, weight) {
    Animal.call(this, color, weight); //绑定 this 这个下面讲
  }
Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.suckle = function () {
  console.log('喝牛奶');
}


同时,爷爷辈的属性,需要继承祖父辈的其他属性,因为上面有讲到:prototype 是继承属性,也可以称之为隐性属性。那么 color, weight 这些显性属性怎么给他继承过来呢?

这个时候就用上了上面的 Mammal.prototype = Object.create(Animal.prototype); 这就是利用 Object.create() 来将祖父的其他显性属性,全部继承到爷爷辈。并且再写进爷爷辈的 prototype 中,方便再往下给爸爸继承。

这样一级一级的绑定,构建,就实现了所谓的 多级继承 了。

当然细心的同学又发现了一个点:

code

 //  Mammal
function Mammal(color, weight) {
  Animal.call(this, color, weight); //绑定 this 
}


为什么这边的爷爷辈的构造器里面为什么要 call this 呢? ,这边就先卖个关子,下面this那段会讲到!嘿嘿~

原型总结

好了,讲了这么多,终于说完了原型链。其实一图胜千言。

引用上面的一句话:每个对象都有一个 proto 的属性,指向该对象的原型。实例后通过对 proto 属性的访问 去对 prototype对象进行访问;原型链是由原型对象组成的,每个对象都有__proto__属性,指向创建该对象的构造函数的原型 ,然后通过__proto__属性将对象链接起来,组成一个原型链,用来实现继承和共享属性!

说到这,原型链也就说完了,接下来再啃一块硬骨头:this

this

其实说到 this,大家都有这样的一个感觉,就是一看就会,一用就乱。那么这个this 到底是个啥?能不能给它整明白?别急,

先来看一段代码:

code

var User = {
 fname:'三'
     lname:'张'
     fullname:function(){
      return User.lname + User.fname
     }
  }
 console.log(User.fullname) // "张三"


这段代码是去获取 User 对象下的全名,可以看到是没什么问题。那么这个时候,需要给这个对象换成person对象,会发生什么呢?

code

var Person = {
 fname:'三'
     lname:'张'
     fullname:function(){
      return User.lname + User.fname
     }
  }
 console.log(Person.fullname) // User is not defined


看到了什么,找不到这个 User,这是为什么呢?很明显,是因为我们再return 中,返回的还是 User 这个对象,但是这个时候,我已经将原来的 User 改成 Person 了。所以,如果这段代码想生效,必须也要将 return 中的 User 对象 改成 Person 对象。

麻不麻烦?可重用性也太低了。那么这个时候,this 就派上用场了。接着看:

code

var Person = {
 fname:'三'
     lname:'张'
     fullname:function(){
      return this.lname + this.fname
     }
  }
 console.log(Person.fullname) // "张三"


这时候,就能看到,我对象名改成了Person,是一样可以拿到这个对象下的 fullname

是不是有同学会问了,这是为什么?其实这个时候,这里面的this,就指向了这个fullname的 fnc 外的Person对象了。是不是觉得说的有点干,那我们就来看看:

code

var Person = {
 fname:'三'
     lname:'张'
     fullname:function(){
         console.log(this) // 在哪边引用this,就在哪边看!
      return this.lname + this.fname
     }
  }
/*
fname:'三'
lname:'张'
fullname:f()
__proto__
      constructor : f Object()
      ...
*/


这样看,是不是十分清晰明了。其实也就是说,我在 fullname 这个方法中使用的 this 就是指向了,我当前这个 function 代码块的上一级。

看到这,是不是感觉明白了?再来:

code

var Person = {
 fname:'三'
     lname:'张'
     fullname:function(){
      return this.lname + this.fname
     }
  }
var  getfullname = Person.fullname // 将Person对象中的fullname 方法,给到新定义的参数使用
console.log(getfullname()) // NAN


这是什么?没拿到 张三 ?这是为啥?

到这里是不是一下子又懵了?这个 this 到底有多少幺蛾子。打印出来看看,这个时候的 this到底是什么:

code

var Person = {
 fname:'三'
     lname:'张'
     fullname:function(){
         console.log(this) 
      return this.lname + this.fname
     }
  }
var  getfullname = Person.fullname // 将Person对象中的fullname 方法,给到新定义的参数使用
console.log(getfullname()) // window:{},NAN


看到什么了?这个 this 竟然指向了window,全局变量。这是咋回事?这就是 this 坑的地方,我上面说到:this 就是指向了,我当前这个 function 代码块的上一级。其实这句话,在这边就直接错了。因为this引用没变。只是我的调用方式变了。

所以这个时候,这句话要重新描述,谨记:this 并不取决于它所在的位置,而是取决于它所在的function是怎么被调用的!!!

而上面 console.log(Person.fullname) // "张三" 可以打印出结果,就是fullname的这个方法,直接被它的父级调用了,也就是说这个时候的 this 是指向的 Person

而如果指定调用这个 this 的,并不是直接父级,那么再非严格模式下,指向的就是全局 window,而在严格模式下则是 undefined

再来 如果 this 再构造函数中被调用,会是怎么样?看下面一段代码 (严格模式下):

code

"use strict";function User (){ console.log(this)}User() // undefined new User () // User {}

这个时候,可以看到,如果 this 是放在构造函数中,被直接调用 User (),那么这个时候的 this 就是 undefined 。因为 this 所在的 function 并没有作为一个方法被调用。

而 如果是通过 new 的方式被调用的,那么这个时候, this 所在的 function 就被调用了,并且指向的就是被调用的 User {} 。还记得我们上面说的,js 本身的构造函数机制吗?再来复习一下:

code 创建对象 "

function User(name, age) {
  this.name = name; // 这里面的this,就代表了即将生成的那个对象 ,并且绑定传参
  this.age = age;
}


就是说:构造函数中的 this ,就是指向即将实例化的那个对象。谨记!

所以 总结一下 this 的三种场景:

1. 如果this 是 在一个函数中,并且被用作方法来叫,那么这个时候的 this 就指向了父级对象;
2. 如果this 是在匿名函数,或者全局环境的函数中,那么这个时候的 this 就是;undefined;
3. 如果this 是在构造函数中,那么这个时候的 this 就指向了即将生成的那个对象


好了,既然区分了 this 的使用场景之后,那么它的强大之处是什么呢?举个例子:

code 动态绑定 this

function introduction() {
  console.log('你好, 我是' + this.name);
}

var zhangsan = {
  name: '张三',
}

var lisi = {
  name: '李四',
}

zhangsan.introduction = introduction;
lisi.introduction = introduction;

zhangsan.introduction(); //  你好,我是张三
lisi.introduction();  //  你好,我是李四


上面可以看到,定义了一个方法,这个方法中使用了 this.name ,但是这个时候,并不知道,这个方法中的 this 到底指向的是谁,而是等待着谁来调用它。回忆一下上面说的那句话:this 并不取决于它所在的位置,而是取决于它所在的function是怎么被调用的!!!

而这个时候,定义了 张三 和 李四 两个对象,这两个对象,分别将定义的 introduction 赋值到本身的对象下面,也就是说,这个时候, 张三 和 李四 两个对象,都拥有了 introduction这个方法,并且调用了。所以,这个时候的 function introduction() 已经拥有了被调用的对象,所以其中的 this.name 也就分别指向了这两个对象的中name。

好,以上就是将 this 的默认指向讲完了。但是是不是有个问题,还没解决?

那就是我们之前在说 多级继承 的时候,有个 call this 。这个卖的关子 还没说呢?那接下来就讲讲。关于 this 改变它的默认指向,绑定一个我想要绑定的环境,行不行?

bind & apply & call

好了,这一段,就接着上面的讲,这里会讲到关于 this 的三种绑定方法。先来看代码:

code 动态绑定 this

function introduction() {
  console.log('你好, 我是' + this.name);
}
introduction() // 你好, 我是 undefined


这个结果相信大家不会陌生,因为就是上面讲的第二种情况:2. 如果this 是在匿名函数,或者全局环境的函数中,那么这个时候的 this 就是;undefined

这里普及一个知识:introduction() === introduction.call() 只是前者是后者的简写!并且call()中的第一个传参可以指定这个函数中的 this 指向谁!

好了,知道这个知识点,再看下面的代码:

code 动态绑定 this

function introduction() {
  console.log('你好, 我是' + this.name);
}
var zhangsan = {
 name:'张三'
}
introduction.call(zhangsan) // 你好, 我是 张三


看完是不是一目了然,这个call()里面传的参数,指向了 zhangsan 这个对象。那这不就是给这个 introduction 方法指定了调用的父级了吗?this 也就指向给调用这个方法的 zhangsan了呀!

说到这是不是就能清楚的知道,这个跟上面 在对象中,来绑定这个方法,来关联父级调用关系,是一样的。一个是对象引用方法,这个就是方法绑定对象呀!

好,再来:

code 动态绑定 this

function introduction(name) {
  console.log('你好,'+ name +' 我是' + this.name);
}
var zhangsan = {
 name:'张三'
}
introduction.call(zhangsan,"李四") // 你好 李四, 我是 张三


可以看到call() 除了可以指定this指向的对象,还可以传一些其他的参数。

好了,说到这,是不是已经能猜到:bind & apply 怎么用拉!

大同小异:

code 动态绑定 this

function introduction(name) {
  console.log('你好,'+ name +' 我是' + this.name);
}
var zhangsan = {
 name:'张三'
}  
introduction.call(zhangsan,"李四")   // 你好 李四, 我是 张三   call
introduction.apply(zhangsan,["李四"])   // 你好 李四, 我是 张三   apply
intro = introduction.bind(zhangsan)
intro("李四")// 你好 李四, 我是 张三   bind


可以看到,call() 和 apply() 区别就在于,后面的传参的格式是:数组的形式;

而 bind() 则是返回一个绑定新环境的 function,等着被调用。

结语

好啦,这期关于 “原型” & “this” 的内容就全部说完了,看到这,就两个字:“透彻”

浏览 40
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报