你不得不懂的 JS 原型和原型链

3,585 阅读8分钟

大家好,我是林一一,这是一篇关于 JS 原型和原型链中的那些事,了解原型和原型链之前需要了解一些 JS 中的OOP,大家也可以直接跳到原型和原型链那里。让我们开始阅读吧。😝

一、本文思维导图

siweidaotu.png

二、面向对象编程 OOP (Object Oriented Programming)

  • 面向过程:就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。

  • 面向对象:是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

  • 面向过程优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用 面向过程开发,性能是最重要的因素。 缺点:没有面向对象易维护、易复用、易扩展

  • 面向对象优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护 缺点:性能比面向过程低

三、JS 中一切都是对象

  • 每一个数据类型底层的封装都是基于 Object 来创建的,引用类型 Function() 函数 Array() Date() Math(),String(), Number(), 等基本类型,nodeLIst 节点集合,window 等都是

  • 基于构造函数 constructor 创建自定义类

// 字面量形式
function fn(){}
var obj = {}

// 构造函数模式
var f = new fn()    // ==>   new fn
console.log(f)

var obj = new Object()
console.log(obj)

使用 new 关键字就是利用构造函数创建一个实例,实例就是一个类。另一个就是字面量形式

  • Object 是一个工厂方法能根据传入的类型,转换成相应的原始包装类型
var num = Object(12)
console.log(num instanceof Number)  // true

var str = Object('12')
console.log(str instanceof String)  // true

var boolean = Object(true)
console.log(boolean instanceof Boolean)  // true
  • 基本类型值创建方式的区别
var a = 12
console.log(typeof a)   // 'number'
a.toFixed(2)    // "12.00"

var b = new Number(12)
console.log(typeof b)   // 'object'
b.toFixed(2)    // "12.00"

var c = new String('12')
console.log(typeof c)  // object

需要注意的是 Symbol() 不支持 new 语法,浏览器不认为 Symbol 是一个构造函数。

思考

问:原始值为什么也可以使用属性或方法 a.toFixed ? 不是说原始值就是一个值没有属性嘛?因为使用 new 关键字创建出来的是一个实例,同时字面量形式创建出来的 a 也是一个实例,实例就有属性和方法。字面量形式的创建实际分为三个步骤,以上面代码为示例

var a = 12
a. toFixed(2)

// 相当于下面
/*
* 1. 创建一个 Number 类型的实例
*  var a = new Number(12)
* 2. 调用实例上的方法或属性
*  a.toFixed(2)
* 3. 销毁实例
*/

a.myPro = '12'
console.log(a.myPro)    // undefined

需要注意的是原始值创建的实例,是只读的,所以不能向实例内添加任何属性或方法。如果添加了属性或方法那也是在当前行内创建一个临时的对象,当前行代码运行结束后这对象就已经被销毁了,例如上面的 a.myPro 是 undefined 《红宝书4 P114页》

四、构造函数的运行机制

function Person(name, age){
    var a = 12
    this.name = name
    this.age = age
}

var person = new Person('林一一', 18)
console.log(person)

思考

1.new Person()这个过程中发生了什么

  • 同样开辟一个私有作用域栈内存,形参赋值和变量提升
  • JS 代码执行之前,构造函数会在当前私有作用域内创建一个对象也就是开辟一个堆内存空间,但暂时不存储任何内容。浏览器会让函数中的主体 this 指向这个堆内存地址
  • 代码自上而下执行
  • 最后代码执行结束后,浏览器会把创建的对像堆内存的对象默认返回,不需要写 return。返回的也就是一个实例

JS 的变量提升机制

constructorPrint.jpg

上面的 this 指向的就是 Person 这个对象,使用 this 的才会给实例创建属性,var a = 12 就不会给实例创建属性 console.log(person.a) ==> undefined,对比 ES6 中的 class。

2.在构造函数中强制 return 返回值会怎么样?

function Person(name, age){
    var a = 12
    this.name = name
    this.age = age
    return '林一一'
    // return {name: '林一一'}
}

var person = new Person('林一一', 18)
console.log(person)

在构造函数中使用的 return 的意义会被剥夺,return '林一一' 的返回值还是一个实例 Person {name: "林一一", age: 18}return {name: '林一一'} 的返回值就是 return {name: '林一一'}

  • 在构造函数中 return 的返回值是原始值时,浏览器返回的还是实例
  • 强制返回一个创建对象时,返回的就是创建的对象。不符合我们想要得到一个类的实例。所以在构造函数中使用 return 是没有多大意义的

五、原型和原型链

JS 中一切皆对象。基本类型,引用类型都是基于 Object 这个基类创建的。函数也是,prototype 的值也是对象类型。

  • 补一个知识盲点:原型 Function.prototype 是一个函数,但是普通函数具备的原型 prototype 是一个对象
typeof Function.prototype // "function"

function fn(){}
typeof fn.prototype // "object"

1. prototype 和 constructor 和 __proto__之间的关联

  • 每一个函数类型都自带一个 prototype 的原型属性,原型是对象类型,浏览器会开辟一个堆内存空间。
  • 浏览器会给这个堆内存空间中添加一个 constructor 的属性,属性值是构造函数本身。构造函数中并没有constructor属性,但是会从构造函数的prototype中查找,obj.constructor===obj.prototype.constructor
  • 每一个对象都有一个 __proto__ 的属性,这个属性指向所创建类的 prototypeprototype 也是对象同样也有 __proto__这个属性。函数是对象吗?是的,所以函数也有 __proto__这个属性。如果不能确定指定的类,那 __proto__ 会指向Object。
  • object 这个基类的本身也是一个函数,所以 __proto__ 指向的是 Function.prototype 的原型,__proto__ 最终指向值是 null
  • 函数的 __proto__ 指向的是 Function.prototype 和 对象的 __proto__ 不一样。Object.__proto__ === Function.prototype => true

but 过 __proto__ 不是实例的属性,也不是构造函数的属性,在大多数的浏览器中都支持这种非正式的访问方式。实际上 __proto__ 来自 Object.prototype,当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)

// 一行代码解析上面的1,2句话
String.prototype.constructor === String   // true

function Fn(){}
var fn = new Fn()

// 对应上面的第三句话
fn.__proto__ === Fn.prototype   // true

//对应上面第四句话
fn.__proto__.__proto__.__proto__ === null   // true

上面代码的成立是因为堆内存中 constructor 的值存储的是函数本身

2. prototype 原型的作用

  • 每一个类都会把公共的属性和方法存储到原型上,给实例调用。
  • 给所创建类的原型 prototype 添加属性和方法就是给实例添加共有方法。
 Object.prototype.myName = '林一一'
 var obj = new Object()
 console.log(obj.myName)

3.原型链机制的查找过程

原型链就是基于 __proto__ 的向上查找机制。当实例操作某个属性或方法时会在当前自己的作用域中查找,找到了则查找结束。没有找到就基于所创建类的原型对象上的 __proto__ 继续向上查找,直到找到基类的 Object.prototype 为止,如果还是没有找到则直接undefined

原型链机制.jpg 图中蓝色的线就是原型链了

4.思考题,下面结果都输出什么,为什么?

function Fn(){
    var a = 12
    this.getName = function(){
        console.log('private getName')
    }
}

Fn.prototype.getName = function (){
      console.log('public getName')
}

var fn = new Fn()
var fn1 = new Fn()
// 1,2
console.log(fn.a)
console.log(fn.getName())
// 3,4,5
console.log(fn.getName === fn1.getName)
console.log(fn.__proto__.getName === fn1.__proto__.getName)
console.log(fn.__proto__.getName === Fn.prototype.getName)
//6,7
console.log(fn.hasOwnProperty ===Object.prototype.hasOwnProperty)
console.log(fn.constructor === Fn)
/* 输出
*   undefined
*   private getName
*   false
*   true
*   true
*   true
*   true
*/

思考模型题.jpg

解答

1中a 并没有使用 this 是不会写入构造函数内的,输出就是undefined,2中 fn.getName() 存在 fn 的私有作用域内输出就是 private getName

3 fn 和 fn1引用堆内存地址不同为false,4中fn 和 fn1 这个实例上的 __proto__指向同一个原型 Fn.prototype 所以为 true。5、同理。

6、fn 中不存在 hasOwnProperty,根据 __proto__向上一级原型Fn.prototype查找也没有,继续根据 __proto__ 向查找到 Object.prototype 找到了 hasOwnProperty,所以输出为true。7同理fn中没有constructor属性,但是会从fn.prototype中查找。

5. constructor 补充

function Fn(){

}

var fn = new Fn()
console.log(fn.constructor === Fn.prototype.constructor)  // true
console.log(fn.__proto__ === Fn.prototype)  // true
console.log(fn.__proto__.constructor === Fn)    // true

fn.constructorfn 并没有constructor 属性,会从 Fn 的原型也就是 Fn.prototype 中读取。

6. 思考 Number.prototype.constructor === Number.constructor 结果相等吗?

Number.prototype.constructor
// Number() { [native code] }

Number.constructor
// Function() { [native code] }

结果是不相等的,Number.prototype.constructor 查找的是自己 Number 原型上的构造函数,Number.constructorNumber 没有 constructor 属性,就会原型链的 __proto__ 向上查找上一级类(这里是函数)的原型,函数的 constructor 指向 Function。所以不同。

六、面试题

求输出结果(字节)

var a = 0;
function Parent() {
  this.a = 1;
  return this;
}

Parent.a = 2;
Parent.prototype = {
  a: 3,
  setA: function (value) {
    this.a = value;  // 这里的 this 会将原实例的属性 a 值给修改了
    return this;
  }
}

console.log(new Parent().a); // 1
console.log(Parent().a); // 1
console.log(new Parent().setA(4).a); // 4
console.log(a); // 1 这里是 1 的原因是上面的 Parent() 执行时,将 a 给修改了
console.log(Parent().setA(5).a); // TypeError

这道题有坑😭,我踩过。这道题需要注意其中函数返回值 this 的指向问题,同时new运算符的优先级比.优先级高,new 的过程中发生了什么事情。其他问题不大,对this指向不熟的看看这里 你不得不懂的 JS this 指向

参考

JavaScript深入之从原型到原型链

更多系列的文章已经放在了 github, 欢迎 start 或 issue

感谢阅读到这里,我是林一一,下次见。