2021高频前端面试题JavaScript:原型与原型链

2021年3月23日11:19:47 发表评论 62 views

1. 对原型、原型链的理解

在JavaScript中使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。

特点: JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。

2. 原型修改、重写

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // false
复制代码

可以看到修改原型的时候p的构造函数不是指向Person了,因为直接给Person的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用constructor指回来:

Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // true

复制代码

3. 原型链指向

p.__proto__  // Person.prototype
Person.prototype.__proto__  // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor  // Person
复制代码

4. 原型链的终点是什么?如何打印出原型链的终点?

由于Object是构造函数,原型链终点是Object.prototype.__proto__,而Object.prototype.__proto__=== null // true,所以,原型链的终点是null 原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是Object.prototype.__proto__" alt="在这里插入图片描述" data-data-original="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8d40a3ff5648431e8da2726d3d4bebe5~tplv-k3u1fbpfcp-zoom-1.image" src="http://www.cainiaoxueyuan.com/wp-content/themes/begin/img/blank.gif" alt="2021高频前端面试题JavaScript:原型与原型链" data-width="800" data-height="600" />

5. 如何获得对象非原型链上的属性?

使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:

function iterate(obj){
   var res=[];
   for(var key in obj){
        if(obj.hasOwnProperty(key))
           res.push(key+': '+obj[key]);
   }
   return res;
} 
复制代码

6. 下面代码的输出结果

function Person(name) {
    this.name = name
}
var p2 = new Person('king');
console.log(p2.__proto__) //Person.prototype
console.log(p2.__proto__.__proto__) //Object.prototype
console.log(p2.__proto__.__proto__.__proto__) // null
console.log(p2.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.__proto__.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.constructor)//Person
console.log(p2.prototype)//undefined p2是实例,没有prototype属性
console.log(Person.constructor)//Function 一个空函数
console.log(Person.prototype)//打印出Person.prototype这个对象里所有的方法和属性
console.log(Person.prototype.constructor)//Person
console.log(Person.prototype.__proto__)// Object.prototype
console.log(Person.__proto__) //Function.prototype
console.log(Function.prototype.__proto__)//Object.prototype
console.log(Function.__proto__)//Function.prototype
console.log(Object.__proto__)//Function.prototype
console.log(Object.prototype.__proto__)//null
复制代码

7. 下面代码的输出结果

function Dog() {
this.name = 'puppy'
}
Dog.prototype.bark = () => {
console.log('woof!woof!')
}
const dog = new Dog()
console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog)
复制代码

打印出:true

因为constructor是prototype上的属性,所以dog.constructor实际上就是指向Dog.prototype.constructor;constructor属性指向构造函数。instanceof而实际检测的是类型是否在实例的原型链上。

constructor是prototype上的属性,这一点很容易被忽略掉。constructor和instanceof 的作用是不同的,感性地来说,constructor的限制比较严格,它只能严格对比对象的构造函数是不是指定的值;而instanceof比较松散,只要检测的类型在原型链上,就会返回true。

8. 下面代码的输出结果

function Dog() {
  this.name = 'puppy'
}
Dog.prototype.bark = () => {
  console.log('woof!woof!')
}
function BigDog() {}
BigDog.prototype = new Dog()
const bigDog = new BigDog()
console.log(bigDog.constructor === BigDog)
复制代码

打印出:false

因为bigDog的原型是Dog的实例,所以访问bigDog.constructor时实际访问的是Dog.prototype.constructor,也就是Dog。所以 bigDog.constructor === Dog,这样才会打印出true。

这个例子证明了单纯用constructor来判断实例的类型是有隐患的,因为bigDog.constructor === BigDog是false,就说明无法用constructor来准确判断bigDog一个实例的类型。

可以这样修改:

function Dog() {
  this.name = 'puppy'
}
Dog.prototype.bark = () => {
  console.log('woof!woof!')
}
function BigDog() {}
BigDog.prototype = new Dog()
// 修复constructor的指向
Object.defineProperty(BigDog.prototype, "constructor", {
        value: BigDog,
        enumerable: false,
      })
const bigDog = new BigDog()
console.log(bigDog.constructor === BigDog)
复制代码

这段代码通过显式地更改BigDog.prototype的指向来修正上面提到的隐患,那为什么要这么大费周章地使用Object.defineProperty,而不是直接这样做呢:

BigDog.prototype.constructor = BigDog
复制代码

是因为不希望 constructor 这个属性被 for…in 遍历到,所以用 defineProperty 来控制访问权限。

for(let i in bigDog){console.log(i)} // 会出现constructor属性
复制代码

如果用上面两个问题去测试,就会发现ES6中的class本质上只是原型链的语法糖。

class Cat {
    constructor(props) {
      this.name = 'puppy'
    }
  }
class BigCat extends Cat {
    constructor(props) {
      super(props)
    }
}
const bigCat = new BigCat()
console.log(bigCat.constructor === BigCat) //true
for(let i in bigCat){console.log(i)} // 不会出现constructor属性

作者:CUGGZ

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: