百行代码带你搞懂原型及原型链

    技术2022-07-11  129

    写在前面

    没有废话,上来就问你什么是原型 什么是原型呢? 你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。 我们先从prototype属性讲起

    prototype

    function Rabbit() {} // by default: // Rabbit.prototype = { constructor: Rabbit }

    每个函数都有 “prototype” 属性,即使我们没有提供它。 需要注意的是prototype是函数才会有的属性。

    但是函数们都有的prototype有什么用呢? prototype属性指向了一个对象,这个对象就是调用该构造函数创建的实例的原型。 也就是:构造函数的prototype属性指向了其实例对象的原型。

    function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); alert(rabbit.eats); // ??? Rabbit.prototype = {}; alert( rabbit.eats ); // ???

    true; true Rabbit.prototype 的赋值操作为新对象设置了 [[Prototype]],但它不影响已有的对象。

    这里又引出了[[Prototype]]

    [[Prototype]]

    属性 [[Prototype]] 是内部的而且是隐藏的,它会引用其他对象。但它不能直接操作 设置它的方式:使用特殊的名字 __proto__

    __proto__ 与 [[Prototype]] 不一样,__proto__ 是 [[Prototype]] 的因历史原因而留下来的 getter/setter

    对象的[[Prototype]]关联也可以修改

    // 方法一 Bar.prototype = Object.create(Foo.prototype); // 方法二,有性能问题 Object.setPrototypeOf(Bar.prototype, Foo.prototype);

    __proto__

    它是每一个JavaScript对象(除了 null )都具有的一个属性,这个属性会指向该对象的原型。

    .__proto__的大致实现

    Object.defineProperty(Object.prototype, "__proto__", { get: function() { return Object.getPrototypeOf(this); }, set: function() { Object.setPrototypeOf(this, o); return o; } });

    .__proto__的替代

    // 创建一个以 animal 为原型的新对象 let rabbit = Object.create(animal); // 返回对象 rabbit 的 [[Prototype]] Object.getPrototypeOf(rabbit) === animal; // 将 rabbit 的原型修改为 {} Object.setPrototypeOf(rabbit, {});

    .__proto__和原型的关系

    大概了解了__prpto__,那么它和原型什么关系呢?

    let arr = [1, 2, 3]; alert( arr.__proto__ === Array.prototype ); // ??? alert( arr.__proto__.__proto__ === Object.prototype ); // ??? alert( arr.__proto__.__proto__.__proto__ ); // ??? alert(Object.prototype.__proto__); // ???

    true true null null arr继承自 Array.prototype 接下来继承自 Object.prototype 原型链的顶端为 null 也就是:实例对象的__proto__属性指向了该对象的原型。

    学习了上面这些概念,来试试下面这道题吧!

    function Rabbit(name) { this.name = name; } Rabbit.prototype.sayHi = function() { alert(this.name); }; let rabbit = new Rabbit("Rabbit"); rabbit.sayHi(); // ??? Rabbit.prototype.sayHi(); // ??? Object.getPrototypeOf(rabbit).sayHi(); // ??? rabbit.__proto__.sayHi(); // ???

    Rabbit undefined undefined undefined 第一个调用中 this == rabbit,其他的 this 等同于 Rabbit.prototype

    __proto__作为键名

    let obj = {}; let key = prompt("What's the key?", "__proto__"); obj[key] = "some value"; alert(obj[key]); // ???

    [object Object],并不是 “some value”! __proto__ 属性必须是对象或者 null。字符串不能成为 prototype。

    let obj = Object.create(null); let key = prompt("What's the key?", "__proto__"); obj[key] = "some value"; alert(obj[key]); // ???

    “some value” 通过 Object.create(null) 来创建没有原型的对象。 使用 “__proto__” 作为键是没有问题的

    通过以上————实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?

    指向实例的属性没有,因为一个构造函数可以生成多个实例指向构造函数的属性倒是有,每个原型都有一个 constructor 属性指向关联的构造函数。

    constructor

    constructor的指向

    function Rabbit() {} alert( Rabbit.prototype.constructor == Rabbit ); // ??? let rabbit = new Rabbit(); // inherits from {constructor: Rabbit} alert( Rabbit.prototype.constructor === rabbit.constructor); // ??? alert(rabbit.constructor === Rabbit); // ???

    true; true; true 默认的 “prototype” 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。 获取 rabbit.constructor 时,其实 rabbit 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 rabbit 的原型也就是 Rabbit.prototype 中读取,正好原型中有该属性。

    function Rabbit() {} Rabbit.prototype = { jumps: true }; let rabbit = new Rabbit(); alert(rabbit.constructor === Rabbit); // ???

    false 将整个默认 prototype 替换掉,那么其中就不会有 “constructor” 了

    function User(name) { this.name = name; } let user = new User('John'); let user2 = new user.constructor('Pete'); alert( user2.name ); // ???

    Pete 我们不触碰默认的 “prototype”,“constructor” 属性具有正确的值 User.prototype.constructor == User

    function User(name) { this.name = name; } User.prototype = {}; let user = new User('John'); let user2 = new user.constructor('Pete'); alert( user2.name ); // ???

    undefined let user2 = new Object(‘Pete’),内建的 Object 构造函数会忽略参数

    function Foo() {/* .. */} Foo.prototype = {/* .. */}; // 创建一个新原型对象 var a1 = new Foo(); a1.constructor === Foo; // ??? a1.constructor === Object; // ???

    false true

    如何保证正确的constructor?

    方法一 // 不要将 Rabbit.prototype 整个覆盖 // 可以向其中添加内容 Rabbit.prototype.jumps = true // 默认的 Rabbit.prototype.constructor 被保留了下来

    方法二 Rabbit.prototype = { jumps: true, constructor: Rabbit }; 手动重新创建 constructor 属性

    方法三 Object.defineProperty(Rabbit.prototype, “constructor”, { enumerable: false, writable: true, configurable: true, value: Rabbit // 让.constructor指向Rabbit }) 设置原型对象的constructor

    原型链

    讲完上面关于原型中的三个属性,我们已经对原型有了大概的认识,下面我们介绍原型链 原型是一个对象,既然是对象,我们就可以用最原始的方式创建它

    var obj = new Object(); obj.name = 'Tom'; console.log(obj.name); // ???

    Tom 其实原型对象就是通过 Object 构造函数生成的,实例的 __proto__ 指向构造函数的 prototype,也就是Person.prototype的__proto__属性指向了Object.prototype Object.prototype的原型是null。

    console.log(Object.prototype.__proto__ === null) // true

    null 表示“没有对象”,即该处不应该有值。 即 Object.prototype.__proto__ 的值为 null 就是 Object.prototype 没有原型。 查找属性的时候查到 Object.prototype 就可以停止查找了。

    再下面就是一些综合型的知识点了。

    综合

    原型中的this

    let user = { name: 'Tom', surname: 'Smith', set fullName(value) { [this.name, this.surname] = value.split(" ") // this是admin,不是user }, get fullName() { return `${this.name} ${this.surname}` } } let admin = { __proto__: user, isAdmin: true } alert(admin.fullName) // ??? alert(user.fullName) // ??? admin.fullName = "Alice Cooper" alert(admin.fullName) // ??? alert(user.fullName) // ???

    Tom Smith Tom Smith Alice Cooper Tom Smith this不受原型影响,始终是点符号 . 前面的对象。(谁调用的方法,谁就是this) setter 调用 admin.fullName= 使用 admin 作为 this,而不是 user 所以admin将仅修改自己的状态,而不会修改user的状态。

    let hamster = { stomach: [], eat(food) { this.stomach.push(food); // * } }; let speedy = { __proto__: hamster }; let lazy = { __proto__: hamster }; speedy.eat("apple"); alert(speedy.stomach); // ??? alert(lazy.stomach); // ???

    apple apple this.stomach.push() 需要找到 stomach 属性,然后对其调用 push,于是顺着原型链向上查找拿到了hamster的stomach属性

    let hamster = { stomach: [], eat(food) { this.stomach = [food]; // * } }; let speedy = { __proto__: hamster }; let lazy = { __proto__: hamster }; speedy.eat("apple"); alert(speedy.stomach); // ??? alert(lazy.stomach); // ???

    alert <nothing> this.stomach= 不会执行对 stomach 的查找。该值会被直接写入 this 对象

    获取实例属性

    alert(Object.keys(rabbit)) // jumps,name,walk

    Object.keys(),仅列出可枚举的、自己的key

    for (let prop in rabbit) alert(prop) // jumps name walk eats sleep

    for…in 循环会迭代继承的属性。(返回所有的key,包括继承来的)

    for (let prop in rabbit) { let own = rabbit.hasOwnProperty(prop) if (own) { alert(`our:${prop}`) // jumps,name,walk } else { alert(prop) // eats sleep 继承来的 } }

    内建方法obj.hasOwnProperty(key)排除继承的属性 rabbit.hasOwnProperty()来自Object,继承而来,不可枚举 同样Object.prototype的其他属性也不会被循环出来

    Object.getOwnPropertyNames(obj)

    Object.getOwnPropertyNames(),列出所有包括constructor

    Reflect.ownKeys(obj)

    返回一个由自身所有键组成的数组。

    let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

    对 obj 进行真正准确地拷贝,包括所有的属性:可枚举和不可枚举的,数据属性和 setters/getters —— 包括所有内容,并带有正确的 [[Prototype]]。 Object.create 有一个可选的第二参数:属性描述器。详细

    判断属性来源

    var a = new Foo(); a instanceof Foo; // true

    instanceof判断在a的整条[[Prototype]]链中是否有Foo.prototype指向的对象 只能处理对象a和函数(带.prototype引用的Foo)之间的关系

    以下判断对象与对象之间的

    function Person() { } Person.prototype.name = 'Tom' Person.prototype.age = 29 Person.prototype.sayName = function () { alert(this.name + '来自原型') } var person1 = new Person() var person2 = new Person() alert(Person.prototype.isPrototypeOf(person1)) // ???

    true isPrototypeOf()判断有无指向关系 在person1的整条[[Prototype]]链中是否出现过Person

    alert(Object.getPrototypeOf(person1) === Person.prototype) // ???

    true Object.getPrototypeOf()获取一个对象的[[Prototype]]链(即获得对象的原型)

    alert(person1.hasOwnProperty('name'));

    true Object.hasOwnProperty(‘属性名’)

    alert("name" in person1);

    true in操作符能够访问到name属性就返回true

    person1.__proto__ ==== Person.prototype; // ???

    true 绝大多数浏览器支持,.__proto__属性引用了内部的[[Prototype]]对象

    写一个函数确定一个属性是不是原型中的属性

    function hasPrototypeProperty(obj, name) { return (name in obj) && !obj.hasOwnProperty(name) }

    in操作符返回true 且 hasOwnProperty()方法返回false

    其他

    性能的角度来看,我们是从对象还是从原型链获取属性都是没区别的。它们(引擎)会记住在哪里找到的该属性,并在下一次请求中重用它。一旦有内容更改,它们就会自动更新内部缓存

    Object.create(…)有轻微性能损失,抛弃的对象需要进行垃圾回收

    更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化。避免修改已存在的对象的 [[Prototype]]

    红宝书中写到 “如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性”

    这样一定会发生屏蔽吗?

    [[Prototype]]链上层存在同名普通数据访问属性并且没有被标记为只读(writable: false)。会屏蔽,同名属性也是屏蔽属性[[Prototype]]链上层存在同名属性但是被标记为只读,那么无法修改已有属性或创建屏蔽属性,严格模式下会报错否则会忽略这条语句。不会屏蔽

    只读属性会阻止[[Prototype]]下层隐式创建(屏蔽)同名属性

    [[Prototype]]链上层存在同名属性并且是一个setter,一定会调用这个setter,这个setter不会被重新定义。不会屏蔽

    参考

    书籍:《JavaScript高级程序设计》、《你不知道的JavaScript》 网站: JavaScript深入之从原型到原型链 现代JavaScript教程

    Processed: 0.021, SQL: 9