2019 面试准备 - JS 原型与原型链

栏目: JavaScript · 发布时间: 6年前

内容简介:Create byRecently revised in如果小伙伴对文章存有疑问,想快速得到回复。

Create by jsliang on 2019-2-21 08:42:02

Recently revised in 2019-2-23 09:44:08

Hello 小伙伴们,如果觉得本文还不错,记得给个star, 你们的star是我学习的动力! GitHub 地址

本文涉及知识点:

  • prototype
  • __proto__
  • new
  • call() / apply() / bind()
  • this

在本文中,jsliang 会讲解通过自我探索后关于上述知识点的个人理解,如有纰漏、疏忽或者误解,欢迎各位小伙伴留言指出。

如果小伙伴对文章存有疑问,想快速得到回复。

或者小伙伴对 jsliang 个人的前端文档库感兴趣,也想将自己的前端知识整理出来。

欢迎加 QQ 群一起探讨: 798961601

一 目录

不折腾的前端,和咸鱼有什么区别

目录

二 前言

广州小伙伴在帮我进行面试摸底的时候,提出了问题: 能否谈谈 this 的作用?

题目的目的:

  1. 了解 this,说一下 this 的作用。
  2. Vue 的 this.变量,this 指向 Vue 的哪里。(指 Vue 的实例)
  3. Vue 里写个 setTimeout,发现 this 改变( call()apply()=>
  4. ……大致如此……

但是,我发现了我走了一条不归路,无意间我看了下 prototype

然后,我爬上了一座高山……

三 题目

相信有的小伙伴能自信地做出下面这些题~

  • 题目 1
var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
  n: 2,
  m: 3
}
var c = new A();

console.log(b.n);
console.log(b.m);

console.log(c.n);
console.log(c.m);
复制代码

请写出上面编程的输出结果是什么?

  • 题目 2
var F = function() {};

Object.prototype.a = function() {
  console.log('a');
};

Function.prototype.b = function() {
  console.log('b');
}

var f = new F();

f.a();
f.b();

F.a();
F.b();
复制代码

请写出上面编程的输出结果是什么?

  • 题目 3
function Person(name) {
    this.name = name
}
let p = new Person('Tom');
复制代码

问题1:1. p.__proto__等于什么?

问题2:Person.__proto__等于什么?

  • 题目 4
var foo = {},
    F = function(){};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';

console.log(foo.a);
console.log(foo.b);

console.log(F.a);
console.log(F.b);
复制代码

请写出上面编程的输出结果是什么?

四 解题

  • 题目 1 答案:
b.n -> 1
b.m -> undefined;

c.n -> 2;
c.m -> 3;
复制代码
  • 题目 2 答案:
f.a() -> a
f.b() -> f.b is not a function

F.a() -> a
F.b() -> b
复制代码
  • 题目 3 答案

答案1:Person.prototype

答案2:Function.prototype

  • 题目 4 答案
foo.a => value a
foo.b => undefined
F.a => value a
F.b => value b
复制代码

如果小伙伴们查看完答案,仍不知道怎么回事,那么,我们扩展下自己的知识点,畅快了解更多地知识吧!

五 知识拓展

原型和原型链估计是老生常谈的话题了,但是还是有很多小白(例如 jsliang 自己)就时常懵逼在这里。

2019 面试准备 - JS 原型与原型链

首图祭祖,让暴风雨来得更猛烈些吧!

5.1 问题少年:旅途开始

因为爱(了解来龙去脉),所以 jsliang 开始学习(百度)之旅,了解原型和原型链。

首先, jsliang 去了解查看原型链 prototype

然后,在了解途中看到了 new ,于是百度查看 JS 的 new 理念。

接着,接触 new 会了解还有 call() ,而 call()apply() 以及箭头函数 => 又是相似的东西。

最后,当我们查找 call() 的时候,它又涉及到了 this ,所以我们 “顺便” 查阅 this 吧。

5.1 原型及原型链

首先,为什么需要原型及原型链?

我们查看一个例子:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.eat = function() {
    console.log(age + "岁的" + name + "在吃饭。");
  }
}

let p1 = new Person("jsliang", 24);
let p2 = new Person("jsliang", 24);

console.log(p1.eat === p2.eat); // false
复制代码

可以看到,对于同一个函数,我们通过 new 生成出来的实例,都会开出新的一块堆区,所以上面代码中 person 1 和 person 2 的吃饭是不同的。

拥有属于自己的东西(例如房子、汽车),这样很好。但它也有不好,毕竟总共就那么点地儿(内存),你不停地建房子,到最后是不是没有空地了?(内存不足)

所以,咱要想个法子,建个类似于共享库的对象(例如把楼房建高),这样就可以在需要的时候,调用一个类似共享库的对象(社区),让实例能够沿着某个线索去找到自己归处。

而这个线索,在前端中就是原型链 prototype

function Person(name) {
  this.name = name;
}

// 通过构造函数的 Person 的 prototype 属性找到 Person 的原型对象
Person.prototype.eat = function() {
  console.log("吃饭");
}

let p1 = new Person("jsliang", 24);
let p2 = new Person("梁峻荣", 24);

console.log(p1.eat === p2.eat); // true
复制代码

看!这样我们就通过分享的形式,让这两个实例对象指向相同的位置了(社区)。

然后,说到这里,我们就兴趣来了, prototype 是什么玩意?居然这么神奇!

孩子没娘,说来话长。首先我们要从 JavaScript 这玩意的诞生说起,但是放这里的话,故事主线就太长了,所以这里有个本文的剧场版 《JavaScript 世界万物诞生记》 ,感兴趣的小伙伴可以去了解一下。这里我们还是看图,并回归本话题:

2019 面试准备 - JS 原型与原型链
  • JS 说,我好寂寞。因为 JS 的本源是空的,即:null。
  • JS 说,要有神。所以它通过万能术 __proto__ 产生了 No1 这号神,即: No1.__proto__ == null
  • JS 说,神你要有自己的想法啊。所以神自己想了个方法,根据自己的原型 prototype 创建了对象 Object ,即: Object.prototype == No1; No1.__proto__ == null 。于是我们把 prototype 叫做原型,就好比 Object 的原型是神,男人的原型是人类一样,同时 __proto__ 叫做原型链,毕竟有了 __proto__ ,对象、神、JS 之间才有联系。这时候 Object.prototype.__proto__ == null
  • JS 说,神你要有更多的想法啊,我把万能术 __proto__ 借你用了。所以神根据 Object ,使用 __proto__ 做了个机器 No2,即 No2.__proto__ == No1 ,并规定所有的东西,通过 __proto__ 可以连接机器,再找到自己,包括 Object 也是,于是 Object 成为所有对象的原型Object.__proto__.__proto__ == No1 ,然后 StringNumberBooleanArray 这些物种也是如此。
  • JS 说,神你的机器好厉害喔!你的机器能不能做出更多的机器啊?神咧嘴一笑:你通过万能术创造了我,我通过自己原型创造了对象。如此,那我造个机器 Function, Function.prototype == No2, Function.__proto__ == No2 ,即 Function.prototype == Function.__proto__ 吧!这样 No2 就成了造机器的机器,它负责管理 Object、Function、String、Number、Boolean、Array 这几个。

最后,说到这里,我们应该很了解开局祭祖的那副图,并有点豁然开朗的感觉,能清楚地了解下面几条公式了:

Object.__proto__ === Function.prototype;
Function.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null;
复制代码

5.3 new 为何物

这时候,我们知道 prototype 以及 __proto__ 是啥了,让我们回归之前的代码:

function Person(name) {
  this.name = name;
}

// 通过构造函数的 Person 的 prototype 属性找到 Person 的原型对象
Person.prototype.eat = function() {
  console.log("吃饭");
}

let p1 = new Person("jsliang", 24);
let p2 = new Person("梁峻荣", 24);

console.log(p1.eat === p2.eat); // true
复制代码

可以看出,这里有个点,我们还不清楚,就是: new 为何物?

首先,我们来讲讲函数: 函数分为构造函数和普通函数

怎么回事呢? No2 始机器 在创造机器 Function 的过程中,创造了过多的机器,为了方便区分这些机器, No1 神 将机器分为两类: 创造物种类的 Function 叫做构造函数(通常面向对象),创造动作类的 Function 叫做普通函数(通常面向过程) 。打个比喻: function Birl() {}function Person() {} 这类以首字母大写形式来定义的,用来定义某个类型物种的,就叫做 构造函数 。而 function fly() {}function eat() {} 这类以首字母小写形式来定义的,用来定义某个动作的,就叫做普通函数。

注意,它们本质还是 Function 中出来的,只是为了方便区分,我们如此命名

然后,我们尝试制作一个会飞的鸟:

// 定义鸟类
function Bird(color) {
  this.color = color;
}

// 定义飞的动作
function fly(bird) {
  console.log(bird + " 飞起来了!");
}
复制代码

接着,我们要使用鸟类这个机器创造一只鸟啊, No1 神 挠挠头,折腾了下( 注意它折腾了下 ),跟我们说使用 new 吧,于是:

// 定义鸟类
function Bird(color) {
  this.color = color;
}

// 创造一只鸟
let bird1 = new Bird('蓝色');

// 定义飞的动作
function fly(bird) {
  console.log(bird.color + "的鸟飞起来了!");
}

fly(bird1); // 蓝色的鸟飞起来了!
复制代码

说到这里,我们知道如何使用类型创造机器和动作创造机器了。

最后,我们如果有兴趣,还可以观察下 No1 神new 内部折腾了啥:

假如我们使用的是: let bird1 = new Bird('蓝色');

// 1. 首先有个类型机器
function ClassMachine() {
  console.log("类型创造机器");
}
// 2. 然后我们定义一个对象物品
let thingOne = {};
// 3. 对象物品通过万能术 __proto__ 指向了类型机器的原型(即 No 2 始机器)
thingOne.__proto__ = ClassMachine.prototype;
// 4. ???
ClassMachine.call(thingOne);
// 5. 定义了类型机器的动作
ClassMachine.prototype.action = function(){
  console.log("动作创造机器");
}
// 6. 这个对象物品执行了动作
thingOne.action();
/*
 * Console:
 * 类型创造机器
 * 动作创造机器
*/
复制代码

OK, new 做了啥, No 1 神安排地明明白白了。

那么下面这个例子,我们也就清楚了:

function Person(name){
    this.name = name
}

Person.prototype = {
  eat:function(){
    console.log('吃饭')
  },
  sleep:function(){
    console.log('睡觉')
  }
};

let p = new Person('梁峻荣',28);

// 访问原型对象
console.log(Person.prototype);
console.log(p.__proto__); // __proto__仅用于测试,不能写在正式代码中

/* Console
  * {eat: ƒ, sleep: ƒ}
  * {eat: ƒ, sleep: ƒ}
*/
复制代码

所以很多人会给出一条公式:

实例的 __proto__ 属性(原型)等于其构造函数的 prototype 属性。

现在理解地妥妥的了吧!

但是,你注意到 new 过程中的点 4 了吗?!!!

5.4 call() 又是啥

在点 4 中,我们使用了 call() 这个方法。

那么, call() 又是啥?

首先,我们要知道 call() 方法是存在于 Funciton 中的, Function.prototype.callƒ call() { [native code] } ,小伙伴可以去控制台打印一下。

然后,我们观察下面的代码:

function fn1() {
  console.log(1);
  this.num = 111;
  this.sayHey = function() {
    console.log("say hey.");
  }
}
function fn2() {
  console.log(2);
  this.num = 222;
  this.sayHello = function() {
    console.log("say hello.");
  }
}
fn1.call(fn2); // 1

fn1(); // 1
fn1.num; // undefined
fn1.sayHey(); // fn1.sayHey is not a function

fn2(); // 2
fn2.num; // 111
fn2.sayHello(); // fn2.sayHello is not a function

fn2.sayHey(); //say hey.
复制代码

通过 fn1.call(fn2) ,我们发现 fn1fn2 都被改变了, call() 就好比一个小三,破坏了 fn1fn2 和睦的家庭。

现在, fn1 除了打印自己的 console,其他的一无所有。而 fn2 拥有了 fn1 console 之外的所有东西: num 以及 sayHello

记住:在这里, call() 改变了 this 的指向。

然后,我们应该顺势看下它源码,搞懂它究竟怎么实现的,但是 jsliang 太菜,看不懂网上关于它源码流程的文章,所以咱们还是多上几个例子,搞懂 call() 能做啥吧~

  • 例子 1:
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

let food1 = new Food('chees', 5);

food1; // Food {name: "chees", price: 5, category: "food"}
复制代码

可以看出,通过在 Food 构造方法里面调用 call() ,成功使 Food 拓展了 name 以及 price 这两个字段。所以:

准则一:可以使用 call() 方法调用父构造函数。

  • 例子 2:
var animals = [
  {
    species: 'Lion',
    name: 'King'
  },
  {
    species: 'Whale',
    name: 'Fail'
  }
]

for(var i = 0; i < animals.length; i++) {
  (function(i) {
    this.print = function() {
      console.log('#' + i + ' ' + this.species + ": " + this.name);
    }
    this.print();
  }).call(animals[i], i);
}

// #0 Lion: King
// #1 Whale: Fail
复制代码

可以看到,在匿名函数中,我们通过 call() ,成功将 animals 中的 this 指向到了匿名函数中,从而循环打印出了值。

准则二:使用 call() 方法调用匿名函数。

  • 例子 3:
function greet() {
  var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
  console.log(reply);
}

var obj = {
  animal: 'cats',
  sleepDuration: '12 and 16 hours'
};

greet.call(obj);  // cats typically sleep between 12 and 16 hours
复制代码

准则三:使用 call() 方法调用函数并且指定上下文的 this

最后,讲到这里,小伙伴们应该知道 call() 的一些用途了。

说到 call() ,我们还要讲讲跟它相似的 apply() ,其实这两者都是相似的,只是 apply() 调用的方式不同,例如:

function add(a, b){
  return a + b;  
}
function sub(a, b){
  return a - b;  
}

// apply() 的用法
var a1 = add.apply(sub, [4, 2]); // sub 调用 add 的方法
var a2 = sub.apply(add, [4, 2]);

a1; // 6     
a2; // 2

// call() 的用法
var a1 = add.call(sub, 4, 2);
复制代码

是的, apply() 只能调用两个参数:新 this 对象和一个数组 argArray 。即: function.call(thisObj, [arg1, arg2]);

以上, 我们知道 apply()call() 都是为了改变某个函数运行时的上下文而存在的(就是为了改变函数内部的 this 指向) 。然后,因为这两个方法会立即调用,所以为了弥补它们的缺失,还有个方法 bind() ,它不会立即调用:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>call()、apply() 以及 bind()</title>
</head>
<body>
  <div id="box">我是一个盒子!</div>
  
  <script>
    window.onload = function() {
      var fn = {
        num: 2,
        fun: function() {
          document.getElementById("box").onclick = (function() {
            console.log(this.num);
          }).bind(this);
          // }).call(this);
          // }).apply(this);
        }
        /*
         * 这里的 this 是 fun,所以可以正确地访问 num,
         * 如果使用 bind(),会在点击之后打印 2;
         * 如果使用 call() 或者 apply(),那么在刷新网页的时候就会打印 2
        */
      }
      fn.fun();
    }
  </script>
</body>
</html>
复制代码

再回想下,为什么会有 call()apply() 呢,我们还会发现它牵扯上了 this 以及箭头函数( => ),所以下面我们来了解了解吧~

5.5 this 指向哪

  • 在绝大多数情况下,函数的调用方式决定了 this 的值。它在全局执行环境中 this 都指向全局对象

怎么理解呢,我们举个例子:

// 在浏览器中, window 对象同时也是全局对象
conosle.log(this === window); // true

a = 'apple';
conosle.log(window.a); // apple

this.b = "banana";
console.log(window.b); // banana
console.log(b); // banana
复制代码

但是,日常工作中,大多数的 this ,都是在函数内部被调用的,而:

  • 在函数内部, this 的值取决于函数被调用的方式。
function showAge(age) {
  this.newAge = age;
  console.log(newAge);
}
showAge("24"); // 24
复制代码

然而,问题总会有的:

  • 一般 this 指向问题,会发生在回调函数中。所以我们在写回调函数时,要注意一下 this 的指向问题。
var obj = {
  birth: 1995,
  getAge: function() {
    var b = this.birth; // 1995;
    var fn = function() {
      return this.birth; 
      // this 指向被改变了!
      // 因为这里重新定义了个 function,
      // 假设它内部有属于自己的 this1,
      // 然后 getAge 的 this 为 this2,
      // 那么,fn 当然奉行就近原则,使用自己的 this,即:this1
    };
    return fn();
  }
}

obj.getAge(); // undefined
复制代码

在这里我们可以看到, fn 中的 this 指向变成 undefined 了。

当然,我们是有补救措施的。

首先,我们使用上面提及的 call()

var obj = {
  birth: 1995,
  getAge: function() {
    var b = this.birth; // 1995
    var fn = function() {
      return this.birth; 
    };
    return fn.call(obj); // 通过 call(),将 obj 的 this 指向了 fn 中
  }
}

obj.getAge(); // 1995
复制代码

然后,我们使用 that 来接盘 this

var obj = {
  birth: 1995,
  getAge: function() {
    var b = this.birth; // 1995
    var that = this; // 将 this 指向丢给 that
    var fn = function() {
      return that.birth; // 通过 that 来寻找到 birth
    };
    return fn();
  }
}

obj.getAge(); // 1995
复制代码

我们通过了 var that = this ,成功在 fn 中引用到了 objbirth

最后,我们还可以使用箭头函数 =>

var obj = {
  birth: 1995,
  getAge: function() {
    var b = this.birth; // 1995
    var fn = () => this.birth;
    return fn();
  }
}
obj.getAge(); // 1995
复制代码

讲到这里,我们再回首 new 那块我们不懂的代码:

// 1. 首先有个类型机器
function ClassMachine() {
  console.log("类型创造机器");
}
// 2. 然后我们定义一个对象物品
let thingOne = {};
// 3. 对象物品通过万能术 __proto__ 指向了类型机器的原型(即 No 2 始机器)
thingOne.__proto__ = ClassMachine.prototype;
// 4. ???
ClassMachine.call(thingOne);
// 5. 定义了类型机器的动作
ClassMachine.prototype.action = function(){
  console.log("动作创造机器");
}
// 6. 这个对象物品执行了动作
thingOne.action();
/*
 * Console:
 * 类型创造机器
 * 动作创造机器
*/
复制代码

很容易理解啊,在第四步中,我们将 ClassMachinethis 变成了 thingOnethis 了!

以上,是不是感觉鬼门关走了一遭,终于成功见到了曙光!!!

六 总结

在开始的时候,也许有的小伙伴,看着看着会迷晕了自己!

不要紧,我也是!

当我跟着自己的思路,一步一步敲下来之后,我才发觉自己仿佛打通了任督二脉,对一些题目有了自己的理解。

所以,最重要的还是 折腾 啦!

毕竟:

不折腾的前端,和咸鱼有什么区别!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Clean Code

Clean Code

Robert C. Martin / Prentice Hall / 2008-8-11 / USD 49.99

Even bad code can function. But if code isn’t clean, it can bring a development organization to its knees. Every year, countless hours and significant resources are lost because of poorly written code......一起来看看 《Clean Code》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具