面试杀手锏:如何用原型说哭面试官

84 阅读2分钟

__protot__和prototype的关系

在 JavaScript 中,__proto__ 和 prototype 都与原型系统相关,但它们的作用和存在位置不同。让我用清晰的示例来解释它们的关系。

核心区别

特性prototype__proto__
所有者只有函数对象才有所有对象都有
作用构造函数创建实例时使用的原型模板对象实际指向的原型
访问方式Func.prototypeobj.__proto__ 或 Object.getPrototypeOf(obj)

原型(显示原型)

  • 函数天生拥有一个属性prototype,新创建对象的隐式原型(__proto__)会指向该函数的prototype对象
  • 意义: 将构造函数中的一个固定属性和方法挂载到原型上,在创建实例的时候,不需要重复执行这些属性和方法
  • 挂载在原型上的属性和方法,在实例对象中可以直接访问到
  • 实例对象无法修改和删除原型上的属性和方法
function Person(name) {
  this.name = name;
}

// 将方法添加到原型上
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const person1 = new Person('Alice');
const person2 = new Person('Bob');

person1.sayHello(); // Hello, my name is Alice
person2.sayHello(); // Hello, my name is Bob

console.log(person1.sayHello === person2.sayHello); // true - 共享同一个方法

对象原型(隐式原型)

  • 正常每一个对象都拥有一个属性__proto__,该属性值是一个对象,这个对象就是当前实例的原型
  • v8在访问对象中的属性时,会先访问对象上显示拥有的属性,如果找不到,就会去对象的隐式原型中查找,也就是__proto__
  • 实例对象的隐式原型===构造函数的显示原型,这是new原理导致的
  • 意义:让实例对象继承到构造函数原型上的属性和方法,方便我们为某一个数据类型添加属性和方法

new构造函数的原理

  1. 创建一个空对象
  2. 让构造函数的this指向这个空对象
  3. 执行构造函数中的代码
  4. 将这个空对象的隐式原型__proto__赋值成构造函数的显示原型
  5. 返回这个对象
function Person(name, age) {
  // new 执行时,this 已经指向新创建的对象
  this.name = name;
  this.age = age;
  
  // 方法最好不要定义在构造函数内(每次new都会创建新函数)
  this.sayName = function() {
    console.log(this.name);
  };
}

// 推荐将方法定义在原型上(所有实例共享)
Person.prototype.sayAge = function() {
  console.log(this.age);
};

// 使用 new 创建实例
const person1 = new Person("Alice", 25);

逐步解析 new Person("Alice", 25) 的执行过程

  1. 创建一个空对象

    javascript

    const obj = {};
    
  2. 将构造函数的 this 指向这个空对象

    // 内部相当于执行了:Person.call(obj, "Alice", 25);
    // this 现在指向 obj
    
  3. 执行构造函数中的代码

    obj.name = "Alice";
    obj.age = 25;
    obj.sayName = function() { console.log(this.name); };
    
  4. 设置对象的原型链

    obj.__proto__ = Person.prototype;
    // 现代浏览器推荐使用:
    // Object.setPrototypeOf(obj, Person.prototype);
    
  5. 返回这个对象

    return obj;
    ### 验证原型链
    

验证原型链

console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
person1.sayAge(); // 25 (从原型继承的方法)

原型链

v8在访问对象中的属性时,会先访问对象上显示拥有的属性,如果找不到,就会去对象的隐式原型中查找,也就是__proto__中查找,如果还是找不到就会在隐式原型的隐式原型中找,层层往上,直到找到null为止

// 祖父辈 - 拥有家族姓氏
const 祖父 = {
  姓氏: '张',
  说姓氏() {
    console.log(`我们家族姓${this.姓氏}`);
  }
};

// 父辈 - 继承了祖父的特性
const 父亲 = Object.create(祖父);
父亲.名字 = '建国';
父亲.自我介绍 = function() {
  console.log(`我叫${this.名字}·${this.姓氏}`);
};

// 子辈 - 继承了父亲的特性
const 儿子 = Object.create(父亲);
儿子.名字 = '小明';

// 现在来看看查找过程
儿子.自我介绍(); // 查找过程如下:
                 // 1. 儿子自身有"名字"属性 → "小明"
                 // 2. 儿子自身没有"姓氏" → 查父亲的__proto__(祖父) → "张"
                 // 3. 儿子自身没有"自我介绍" → 查父亲 → 找到并执行
                 // 输出: "我叫小明·张"

儿子.说姓氏();   // 1. 儿子自身没有"说姓氏"
                 // 2. 查父亲 → 没有
                 // 3. 查祖父 → 找到并执行
                 // 输出: "我们家族姓张"

console.log(儿子.职业); // 1. 儿子自身没有
                       // 2. 父亲没有
                       // 3. 祖父没有
                       // 4. 祖父的__proto__是Object.prototype
                       // 5. Object.prototype的__proto__是null
                       // 最终返回undefined

没有原型的对象

  • 没有原型的对象就是Object的实例,Object的原型是null
  • Object.create(obj)创建一个新对象,让这个新对象的隐式原型等于传入的obj
  • Object.create(null)得到一个没有原型的对象
const pureObj = Object.create(null);

// 验证
console.log(pureObj.toString); // undefined
console.log(Object.getPrototypeOf(pureObj)); // null

constructor

constructor 的存在是为了让所有实例对象都知道自己是从哪个构造函数创建的

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

const alice = new Person('Alice');

// 通过 constructor 属性可以知道对象是由哪个构造函数创建的
console.log(alice.constructor === Person); // true