【跟着犀牛书复习JS基础】搞透大Object之对象原型与原型链、对象建立与构造函数、对象属性与属性继承、属性特性与相关API总结

引言javascript

最近比较忙致使这篇拖了很久啊,第二篇的做用域和闭包由于其中一部分没搞得很清楚也很难受,决定不和本身钻牛角尖了,本篇最后的面试题部分会包含一部分闭包的知识点以弥补上篇没讲清和讲的不够详细的知识点。java

本篇对标犀牛书第6大章和第9大章git

为何叫大Object,事实上JS将它单独做为一个基本数据类型应该就足以称之为了,也够复杂。撰写本篇的初心仍是想搞清楚原型和原型链因此放在最前面,只是在看的过程当中发现和相关的知识点很成体系以及也比较重要,因此都总结记录了一下,能够做为补充看。github

原型和原型链

_proto_prototypeconstructor属性

每一个JS对象(null除外)都自动拥有一个_proto_属性,这个属性是一个对象,指向该对象的原型面试

每一个JS函数(bind()方法除外)都自动拥有一个prototype属性,这个属性也是一个对象,用该函数作构造函数建立的对象将继承这个prototype的属性,也就是说理论上任何一个JS函数均可以用做构造函数,而且调用构造函数须要用到prototype属性。浏览器

prototype属性包含一个惟一不可枚举的属性constructor,这个属性是一个函数对象,指向该函数的构造函数。闭包

看完上述两点你可能会认为_proto_是否是就是对象的原型,prototype是否是就是函数的原型呢? 事实上并非,但咱们能够说对象的_proto_属性指向它的原型,构造函数的prototype属性指向调用构造函数建立的实例的原型。函数

这么说有点绕,用图片(来源:github.com/mqyqingfeng… 侵删)表示即为:ui

实例原型与构造函数的关系图

综上所述,若是咱们有一段代码:this

function Person(name, age){
  this.name = name;
  this.age = age;
}
let person = new Person('xiao hong', 18);
复制代码

能够获得:

person._proto_ === Person.prototype;
Person.prototype.constructor === Person;
复制代码

原型链

咱们知道,当执行属性访问表达式时,首先会将表达式的操做主题转化为对象,而后去对象中查找属性,若是找不到就去找与对象的原型中的属性,若是还找不到,就继续查找原型的原型,直到找到最顶层为止。

那么原型的原型是什么?咱们知道,_proto_prototype属性也只是普通的对象而已,既然是对象,就也有_proto_属性,一个普通对象的_proto_属性天然指向其构造函数Obejct的prototype属性,即Object.prototype

Object.prototype的原型呢?咱们能够打印一下:

console.log(Object.prototype._proto_) // null
复制代码

因此,当咱们向上查找属性的时候,查到Object.prototype就能够中止了,由这些对象相互关联的原型之间的关系就是原型链。到这里,咱们的图片来源:github.com/mqyqingfeng… 侵删)也能够更新为:

原型链示意图

综上所述,若是咱们有一段代码:

function Person(name, age){
  this.name = name;
  this.age = age;
}
Person.prototype = {
  getName: function(){return this.name}
}
let person = new Person('xiao hong', 18);
person.toString();
复制代码

咱们在person上找不到toString方法,就会去person的原型上找,person._proto_ === Person.prototype,结果Person.prototype上也没有这个方法,就会再去原型的原型上找即Person.prototype._proto_, 要知道Person.prototype._proto_只是一个普通的对象,能够被原始的new Object()建立,因此Person.prototype._proto === Object.prototype,所幸Object.prototype上有toString()方法,所以调用它,查找到此结束。

补充

  1. constructor属性

不是每一个对象都有constructor属性,所以不是每一个对象均可以用做构造函数的prototype属性对象,但能够显示的定义constructor属性反向引用构造函数来修正这个问题。

constructor属性也是普通的对象属性,若是找不到改属性,也会从对象原型上继续寻找。以下:

function Person() {

}
var person = new Person();
console.log(person.constructor === Person); // true
复制代码

当获取 person.constructor 时,其实 person 中并无 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,因此:

person.constructor === Person.prototype.constructor
复制代码
  1. _proto_

其次是 __proto__ ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于Person.prototype中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用obj.__proto__ 时,能够理解成返回了 Object.getPrototypeOf(obj)

  1. Function._proto_ === Function.prototype

这里并非由于Function是对象,有_proto_属性,Function又是函数,函数对象的原型指向其构造函数Function.prototype,这样理解虽然看上去很正确可是是不对的。引用大佬的话:

Function.prototype是引擎创造出来的对象,一开始就有了,又由于其余的构造函数均可以经过原型链找到Function.prototype,Function自己也是一个构造函数,为了避免产生混乱,就将这两个联系到一块儿了

  1. Object.__proto__ === Function.prototype

Object是对象的构造函数,那么它也是一个函数,固然它的__proto__也是指向Function.prototype

实际上原型原型链就是这样,但若是你以为上述仍是很难理解,最好再理解一系列的概念,如下内容能够做为做为对原型和继承的补充理解:

对象建立与构造函数

对象的三种建立方法:

对象直接量

例如let o = {},对象直接量是一个表达式,这个表达式的每次运算都会建立并初始化一个新的对象,也就是说在一个函数中使用对象直接量会函数重复调用时建立不少新对象

new+构造函数建立对象

例如let o = new Object()

  • 其中new关键字作了什么呢,根据MDN的介绍

![image-20191026185858754](/Users/chenxingjian/Library/Application Support/typora-user-images/image-20191026185858754.png)其中第2点即,设置建立的新对象的原型与构造函数的prototype属性相关联。

注意:new关键字也不是必定建立一个新对象的,例如:new Object({}), 根据ES5规范,若是new Object(value)中检测到Value的类型为object就直接返回该对象而不会建立一个新对象。

  • 其中构造函数作了什么呢,首先明确 构造函数不是必定要和new关键字一块儿使用的,当构造函数作为函数单独调用时,作了一些不一样的事,好比内置构造函数若是不和new一块儿使用则将参数作一次类型转换,但不必定建立一个新对象,至于具体的差异,推荐食用http://yanhaijing.com/es5/#334

补充:new运算符优先级

优先级 运算类型 关联性 例子
20 圆括号 n/a (a + b) * c
19 成员访问 从左到右 object.method
19 须要计算的成员访问 从左到右 object[“a”+”b”]
19 new 带参数列表 n/a new fun()
19 函数调用 从左到右 fun()
18 new 无参数列表 从右到左 new fun

思考:new Foo().getName()new Foo.getName()两个表达式中的运算优先级

咱们知道,构造函数没有参数列表的时候是能够省略括号的 也就是 new Foo() 等同于 new Foo,根据上表,优先级越高的先执行:

new Foo().getName()表达式中,new 带参数列表优先级高于Foo()函数调用表达式,先执行new Foo(),即表达式等同于(new Foo()).getName()

new Foo.getName()表达式中,成员访问表达式优先级高于new 无带参列表,即表达式等同于new (Foo.getName())

Object.create()函数

该函数接受两个参数,而且返回一个新建立的对象,而且将第一个参数做为新建立的对象的原型,甚至能够传入null来建立一个没有原型的空对象,这样建立的空对象将不继承任何基础方法,好比toString,这意味着这样建立的对象将没法和+一块儿正常工做。

对象属性与属性继承

对象属性

对象的三个属性 :原型属性、类属性、可扩展性。

原型

对象有自有属性和继承属性,其中原型属性就是做为继承属性来使用的。经过new建立的对象用构造函数的prototype属性做为对象的原型,经过Object.create()建立的对象使用第一个参数做为建立对象的原型,没有原型的对象为数很少,其中包括Object.prototypeObject.create(null)

能够用a.isPrototypeOf(b)方法判断a是不是b原型,即b是否继承自a.

能够用Object.getPrototypeOf(a)来获取a对象的原型,若a不是对象类型则抛出类型错误。

一般和构造函数的名称保持一致,经过内置构造函数建立的对象有类名,而且能够经过相似Object.prototype.toString.call(new Date())的方法来得到类名,而自定义的对象没有类名,由于类属性必定为“Object”。

扩展性

宿主对象的可扩展性由js引擎决定(任何对象,不是原生对象就是宿主对象),ES5中,全部内置对象和自定义对象都是可扩展的。除非将其转换为不可扩展的。

可使用Object.isExtensible()来检测对象是否可扩展,使用Object.preventExtensions()来将对象转换为不可扩展的。一旦将对象转换为不可扩展的就没法再转换为可扩展的。不可扩展只对于对象的自有属性,若是对象的原型扩展了方法那么该对象将仍然继承该方法。

补充

存取器属性getter/setter

若是一个属性同时具备getter/setter方法,则该属性具备读/写性,若是只有getter方法,则是只读属性,若是只有setter方法,则是只写属性。

存取器属性是可继承的,使用方法以下实例:

var o = {
  x: 1,
  get y(){return this.x},
  set y(value){this.x = value}
}; //{x: 1, y: 1}
o.y = 2; //{x: 2, y: 2}

var o = {x: 1, get y(){return this.y}}
o.y //RangeError: Maximum call stack size exceeded 读取器里读取它本身无限回调

var o = {x: 1, set y(value){return value}}
o.y = 3;
o.y // undefined 读取只写属性永远返回undefined

var o = {x: 1, set y(){return value}} //Setter must have exactly one formal parameter
复制代码

属性特性与相关API总结

由上述可知,getter/setter存取器属性与属性的可读可写性密切相关,因此可视为属性的特性。

普通属性的四个特性:值、可写性、可枚举型、可配置性。分别对应{value, writable, enumerable, configurable}

读取器属性的四个特性:读取、写入、可枚举性、可配置型。分别对应{get, set, enmurable, configurable }

能够经过Object.getOwnPropertyDescriptor({x: 1}, x)查看对象特定自有属性特性。若是要查看继承属性,须要遍历原型链Object.getPrototypeOf()

能够经过Object.definedProperty(o, "x", {writable: false})新建或修改某对象特定自有属性的特性。对于新建的属性来讲,第三个参数中不存在的特性将被描述为falseundefined,对于修改的属性来讲,第三个参数中不存在的特性将不会被修改。

能够经过Object.seal()将对象设置为封闭的,即不可扩展的以及将对象属性设置为不可配置的,相对于Object.preventExtensions()方法,Object.seal()方法处理的对象将不能删除和配置已有属性,但可写属性依然能够修改。封闭的对象将不能解封,能够用Object.isSealed()方法检测对象是不是封闭的

能够经过Object.freeze()方法将对象冻结,即不可扩展的以及将对象属性设置为不可配置的还将全部数据属性设置为只读的(对setter属性无效),可使用Object.isFrozen()检测对象是否冻结。

面试题练习

  1. 写出下面代码的执行结果
function Foo() {
  getName = function() {
    console.log(1);
  }
  return this;
}
Foo.getName = function() {
  console.log(2);
}
Foo.prototype.getName = function() {
  console.log(3);
}
var getName = function() {
  console.log(4);
}
function getName() {
  console.log(5);
}
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
复制代码
  1. 写出下面代码的执行结果
var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
  n: 2,
  m: 3
}
var c = new A();

console.log(b.n);
console.log(b.m);

console.log(c.n);
console.log(c.m);
复制代码
  1. 写出下面代码的执行结果
var F = function() {};

Object.prototype.a = function() {
  console.log('a');
};

Function.prototype.b = function() {
  console.log('b');
}

var f = new F();

f.a();
f.b();

F.a();
F.b();
复制代码
  1. 回答如下两个问题
function Person(name) {
    this.name = name
}
let p = new Person('Tom');

//问题1:1. p.__proto__等于什么?

//问题2:Person.__proto__等于什么?
复制代码
  1. 写出如下代码的执行结果
var foo = {},
    F = function(){};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';

console.log(foo.a);
console.log(foo.b);

console.log(F.a);
console.log(F.b);
复制代码

解析

  1. 本题考察的知识点不少,包括函数声明提早,原型链,执行上下文this,所以放在了本篇。

    • Foo.getName() Foo对象上有getName属性,直接调用执行输出2

    • getName() 这里考察声明提早, 函数声明提早但函数定义表达式不提早,所以这一段实际被编译为:

      var getName;
      function getName(){
        console.log(5);
      }
      getName = function(){
        console.log(4);
      }
      getName();
      复制代码

      所以输出4

    • Foo().getName() F()给一个未声明的变量getName赋值了一个函数,实际建立了一个同名的全局对象属性getName并赋值为function(){console.log(1)},又由于函数是普通调用,没有绑定在对象上或实例上,返回的this即window,调用window.getName() 输出1

    • Foo()建立的全局getName属性覆盖了定义的函数声明,输出1

    • new Foo.getName() 注意运算优先级 先计算Foo.getName() 输出 2

    • new Foo().getName() 注意运算优先级,先执行new Foo(),new Foo()建立一个新对象,对象关联到 Foo.prototype,返回以新建立的对象为上下文的this,所以调用Foo.prototype.getName() 输出3

    • new new Foo().getName() 执行顺序new ( (new Foo()).getName()) 同上输出3

  2. 本题考察了原型链。

    var A = function() {};
    A.prototype = {constructor: function(){}};
    A.prototype = {constructor: function(){}, n: 1};
    var b = new A();
    b._proto_ = A.prototype;  
    b._proto_ = {constructor: function(){}, n: 1};
    
    A.prototype = {
      n: 2,
      m: 3
    };
    var c = new A();
    c._proto_ = A.prototype = {
      n: 2,
      m: 3
    };
    
    b.n = 1; b.m = undefined;
    c.n = 2; c.m = 3;
    复制代码
  3. 考察原型链

    F.prototype = {contructor: function(){}};
    Object.prototype.a = function() {
      console.log('a');
    };
    Function.prototype.b = function() {
      console.log('b');
    }
    f._proto_ = F.prototype;
    f.a => f._proto_.a => F.prototype.a => F.prototype._proto_.a => Object.prototype.a
    f.a()//'a'
    
    f.b => f._proto_.b => F.prototype.b => F.prototype._proto_.b =>
    Object.prototype.b
    f.b()// TypeError undefined is not a function
    
    F.a => F._proto_.a => Function.prototype.a => Function.prototype._proto_.a => Object.prototype.a
    F.a() // 'a'
    
    F.b => F._proto_.b => Function.prototype.b
    F.b() //'b'
    复制代码
  4. (1) p._proto_ = Person.prototype;

    (2)Person._proto_ = Function.prototype;

  5. 以下:

foo.a => foo._proto_.a => Object.prototype.a => 'value a'
foo.b => foo._proto_.b => Object.prototype.b => undefined

F.a => Foo._proto_.a => Function.prototype.a => Function.prototype._proto_.a => Object.prototype.a => 'value a'
F.b => Foo._proto_.b => Function.prototype.b => 'value b' 
复制代码
相关文章
相关标签/搜索