2023-06-03 终极理解JavaScript基础之-原型&作用域&相等性比较

167 阅读5分钟

什么是原型?

  • 所有的对象都是通过new 函数创建的

  • 所有的函数也是对象

    • 函数中可以有属性
  • 所有的对象都是引用类型

var obj = {};

console.log(obj);

通过字面量创建的对象本质上也是通过new Object函数创建的,只是简化了写法而已,数组字面量写法var arr = []其实也是通过new Array函数来创建的

那么new 函数又是什么呢?

如下:

function test() {
    return {}; // 等同于new Object()
  }
  var t = new test();
  console.log(t);

如果new函数返回一个对象,那么t就是这个对象;如果不是对象,那么t就是test构造出的this对象

image.png

image.png

根据MDN的官方文档描述,new进行的操作如下:

  1. 创建一个空的简单JavaScript对象(即{})
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象
  3. 将步骤1新创建的对象作为this的上下文
  4. 如果该函数没有返回对象,则返回this

这里一张图概括: image.png

注意:Function比较特殊,是直接放到内存中,没有任何东西来创建Function,所有其他的函数都是通过new Function来创建的,对象是通过new Object创建的,像Object、Array这些都是函数

image.png

显式原型-prototype

所有函数都有一个属性:prototype称之为函数原型,如Object.prototypeArray.prototype

默认情况下,prototype是一个普通的Object对象

默认情况下,prototype中有一个属性constructor,它也是一个对象,指向构造函数本身

image.png

image.png

隐式原型-__proto__

所有的对象都有一个属性:__proto__,称之为隐式原型,在控制台中表现为[[Prototype]]属性

由于函数也是对象,所以函数必有__proto__属性,但是对象不一定是函数,所以对象不一定有prototype属性,即普通对象没有prototype

image.png

默认情况下,隐式原型指向创建该对象的函数的原型,即函数的显式原型

image.png

有道面试题如下:

function create() {
    if (Math.random() > 0.5) {
      return [];
    } else {
      return {};
    }
  }

  var obj = new create();
  // 如果得到obj的构造函数的名称
  console.log(obj.__proto__.constructor.name);

原型链又是啥?

当访问一个对象的成员时:

  1. 看该对象自身是否拥有该成员,如果有直接使用
  2. 看该对象的隐式原型是否拥有该成员,如果有直接使用
  3. 在原型链中依次查找

如下图:

image.png

特殊点:

  1. Function的__proto__指向自身的原型,因为没有任何东西创建它,__proto__的本质是谁创建它,这个属性就指向创建者的原型对象
  2. Object的prototype__proto__指向null

作用域

  1. JS有两种作用域:全局作用域和函数作用域

    • 内部的作用域能访问外部,反之不行。访问时从内向外依次查找
    • 如果在内部的作用域访问了外部,则会产生闭包
    • 内部作用域能访问的外部,取决于函数定义的位置,和调用无关
  2. 作用域内定义的变量、函数声明会提升到作用域顶部

一道关于作用域面试题

var a = 1;

  function m() {
    a++; // 2
  }

  function m2() {
    var a = 3;
    m();
    console.log(a); // 3
  }

  m2();
  console.log(a); // 2

image.png

这里m()是定义时候就有了作用域,跟在哪儿调用的无关

全局对象

无论是浏览器环境还是node环境,都会提供一个全局对象

  • 浏览器环境:window
  • node环境:global

全局对象有以下几个特点:

  • 全局对象的属性可以被直接访问

  • 给未声明的变量赋值,实际就是给全局对象的变量赋值

  • 所有的全局变量,全局函数都会附加到全局对象

      这里称之为全局污染,又称之为全局暴露,或简称污染、暴露
      如果要避免污染,需要使用立即执行函数改变其作用域
      立即执行函数又称之为IIFE,它的全称为Immediately Invoked Function Expression
      IIFE通常用于强行改变作用域
    

IIFE通常用法:

var abc = (function () {
    var a = 1; // 不希望污染全局
    var b = 2; // 不希望污染全局

    function c() {
      console.log(a + b);
    }

    return c;
  })();

ES6中的let&const

在开发中,常量用const,变量用let

两者都有以下特点:

  • 全局定义的变量不再作为属性添加到全局对象中
  • 在变量定义之前使用它会报错
  • 不可重复定义同名变量
  • 使用const定义变量时,必须初始化
  • 变量具有块级作用域,在代码块之外不可使用

相等性比较-这里考虑==

从上到下按照规则比较,直到能得到确切结果为止:

  1. 两端类型相同,比较值(引用类型比较的是地址值,等同于严格相等)

  2. 两端存在NaN,返回false

  3. undefined和null只有与自身比较,或者互相比较时,才会返回true

  4. 两端都是原始类型,转换称数字比较

  5. 一端是原始类型,一端是对象类型,把对象转换成原始类型后进入第1步

     对象如何转原始类型?
     1. 如果对象拥有[Symbol.toPrimitive]方法,调用该方法,若该方法能得到原始值,使用该原始值;若得不到原始值,抛出异常
     2. 调用对象的valueOf方法,若该方法能得到原始值,使用该原始值;若得不到原始值,进入下一步
     3. 调用对象的toString方法,若该方法能得到原始值,使用该原始值;若得不到原始值,抛出异常
    

一道经典面试题:

/**
 * 相等性==面试题
 * 如何让下面的判断成立
 */

const a = {
  i: 1,
  // 这个方法有,调用得到原始值用,否则报错
  /* [Symbol.toPrimitive]() {
    console.log("toPrimitive");
    return this;
  }, */
  // 上述方法不存在用这个方法,只要是对象都有,因为是Object.prototype.valueOf,得到原始值用,得不到往下
  valueOf() {
    console.log("valueOf");
    return this.i++;
  }
  // 上述得不到原始值,调用这个方法,得到原始值用,得不到报错
  /* toString() {
    console.log("toString");
    return 1;
  } */
};

if (a == 1 && a == 2 && a == 3) {
  console.log("你牛逼");
}

不得不说不管哪行哪业,只要掌握了事物的基本原理,一切问题都能迎刃而解,但往往就是这个最底层的原理是似懂非懂,需要时间去打磨,去深刻理解。