Javascript之其实我觉得原型链没有难的那么夸张!

  原型链、闭包、事件循环等,可以说是js中比较复杂的知识了,复杂的不是因为它的概念,而是因为它们本身都涉及到很多的知识体系。所以很难串联起来,有一个完整的思路、脉络。我最近想把js中有意思的知识点都总结整理一下,虽然逃不开一些一模一样的内容,但是自己造一下轮子,按照自己的思路,也别有一番味道。

  这篇文章总体来说,是讲原型链的,但是并不涉及到继承。继承的问题,后面会专门拿出来一篇文章来说。这篇文章中的很大一部分,也并不完全是“原型”,还涉及到很多前置的知识。文章有点长,希望你能耐心读完,吸收之后肯定会有不小的收获!那么,我们就先从一个简单的问题开始这篇万字(确实差不多有1w字,别怕,我在)长文吧!

 

一、请描述一下js的数据类型有哪些?

  就这?这么简单的么?哈哈哈...我们先从这个问题开始。

  答:js的数据类型有字符串String数值Number布尔值BooleanNullUndefined对象Object、还要加上SymbolBigInt。一共就这些,Symbol不用说,大家都比较熟悉了,BigInt是后来又加上的“大数”类型。现代浏览器也是支持的。这些数据类型中,又分成了两类,我比较喜欢叫做值类型(String、Number、BigInt、Boolean、Symbol、Null、Undefined)和引用类型(Object)。也或许有人喜欢叫做简单类型和复杂类型。但是我觉得这样形容比较模糊。“值”和“引用”或许更贴切一些。

  到这里,本该告一段落,但这里我挖了一个小小的坑,我问的是js的数据类型,实际上,我上面所说的这些数据类型,在js的规范里,叫做语言类型语言类型是什么意思呢?我们大胆猜测一下,语言类型就是指,我们在日常开发的代码中所书写的基本的数据类型,就叫做语言类型,它是我们在使用这门语言的时候,所采用、依照的数据类型。

  那...你的意思是说,还有另外一种类型?是的,在js的规范中,还有一种类型叫做规范类型,规范类型是干什么用的呢?规范类型对应于算法中用于描述ECMAScript语言构造和ECMAScript语言类型语义的单元。(A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types.)

  什么意思呢?简单来说,规范类型就是在语言背后运行时,所使用的、我们无法获取或“见到”的数据类型。规范类型大概有以下几种:

  1. The Set and Relation Specification Types

  2. The List and Record Specification Types

  3. The Completion Record Specification Type

  4. The Reference Specification Type

  5. The Property Descriptor Specification Type

  6. The Environment Record Specification Type

  7. The Abstract Closure Specification Type

  8. Data Blocks

  一共就这八种,那具体这些规范类型是做什么的,以及怎么用,这里不详细说,有兴趣的可以在链接中找到,免得说多了就有点主次不分了,我们仅仅只是在聊数据类型的时候,把规范类型提一下。

  ok,我们现在知道了js的语言类型有哪些,但是这里又出现了一个问题,就是我怎么判断一个数据是什么类型呢?也就是传说中的“我是谁”的问题!

 

二、我是谁之typeof

  typeof想必大家都比较熟悉了,它能判断一个“数据”的类型,但是大家也知道,typeof并不能判断所有的“类型”(其实,typeof是可以判断所有的类型的,当然,这个“所有类型”的前提是“基本数据类型”,而并不包括构造函数、包装函数等创建的对象)。我们先来看张表,下面是typeof运算符的所有的结果集

  

typeof val Result
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Symbol "symbol"
BigInt "bigint"
Object (does not implement [[Call]]) "object"
Object (implements [[Call]]) "function"

  我们再看下实际的运算结果,毕竟我逼逼逼逼,也不如show 下 code:

console.log(typeof 1);
console.log(typeof "1");
console.log(typeof true);
console.log(typeof Symbol("我是Symbol"));
console.log(typeof null);
console.log(typeof undefined);
console.log(typeof BigInt('1111111111111111'));
console.log(typeof function () { });
console.log(typeof {});

  上面打印的结果是这样的:

number
string
boolean
symbol
object
undefined
bigint
function
object

  null为啥是object我不想说了。要强调的是,以上的“结果”,都是字符串。console.log(typeof typeof {})。这句话,会告诉你typeof的所有结果的类型都是字符串。

  不知道你们从结果和上面的表格中发没发现一个问题,就是Object (does not implement [[Call]])Object (implements [[Call]])的结果竟然是不一样的,一个是function,一个是object?这是怎么回事?从英文的翻译来看,解释为:如果在该对象上(或其原型链上)可以找到[[call]]私有方法,那么就是typeof的结果就是function,如果找不到,那么结果就是object

  哦...原来是这样,也就是说,实际上Object有两种结果...一个object,一个function。那我怎么区分呢?我怎么知道到底是object还是function。我怎么知道它是对象还是函数?我们继续往下看。

 

三、万物皆对象

  想必无论是js的初学者还是资深大师,都一定听说过,在js里,一切皆对象。是嘛?那前面说的值类型的数据也是对象么?是的,它们也可以算是一种对象!我们后面会详聊。现在我只想专心的聊聊对象。

三点一:什么是对象?

  这个有点复杂,我所理解的对象是这样的:使用new运算符,通过构造函数创建的一个包含一系列属性集合的数据类型。但是这只是一个片面的解释,实际上,如果忽略对象的创建过程,即我们不去纠结它是怎么来的,只是专注于它是什么,那么对象就是具有唯一性的、属性和行为的集合,仅此而已。

三点二:对象的种类有哪些?

  其实对象的种类有很多,而且大多数在我们开发的时候已经经常使用了,只是我们从未真正的去做一个比较区分罢了。要知道,从不同的角度看待同一个问题,结果也会有所区别,所以,从不同的角度去区分类别,结果也不尽相同的。比如,按照构造函数的角度区分,可以分为函数对象(具有[[call]]私有字段,表面上来看,就是可以调用call方法的函数)构造器对象(具有[[constuctor]]私有字段,表面上来看,就是具有constructor属性的函数)。我们仅从比较大众和公认的角度,对象大概的分类如下(其实不想要复杂的理解的话,就是宿主对象和内置对象两种,再往细了分,实际上没有特别巨大的区别,它们本质上极其类似):

  下面,我们都简单解释下,弄清楚对象的分类,对于后面的学习,会更加深入和清晰。

  简单来说,宿主即JavaScript代码所运行的载体,大多数时候是浏览器,但是也可能是node或其他复杂的环境上。而JavaScript是可以使用“该环境”的相关对象的,即称为宿主对象。宿主对象本身又分为固有和用户可创建两种。无需多说。

  而内置对象,则是JavaScript本身内置(built-in)的对象,与运行载体无关。其中内置对象又可以分为三种,即:固有对象、原生对象、普通对象。

  普通对象最好理解,就是我们通过对象字面量、Object构造器、Class关键字定义类创建的对象,它能够被原型继承,换句话说,就是我们使用的最简单的直接的对象形式。

  固有对象由标准规定,随着JavaScript运行时创建而自动创建的对象实例。固有对象在任何JavaScript代码执行前就已经创建了,它们通常扮演着基础库的角色。类其实就是固有对象的一种,固有对象目前有150多种。

  原生对象,即可以通过原生构造器创建的对象。标准中,提供了30多个构造器,通过这些构造器,可以使用new 运算创建新的对象,所以我们把这些对象称作原生对象。这些构造器创建的对象多数使用了私有字段:

  * Error:[[ErrorData]]
  * Boolean:[[BooleanData]]
  * Number:[[NumberData]]
  * Date:[[DateValue]]
  * RegExp:[[RegExpMatcher]]
  * Symbol:[[SymbolData]]
  * Map:[[MapData]]

  这些字段使得原型继承方法无法正常工作,所以,我们可以认为这些原生对象都是为了特定能力或者性能,而设计出来的特权对象。

三点三:值类型也是Object?么?

  那么值类型?值也是对象么?下面的代码就可以解释这个问题。

var objNum = new Number(1);
console.log(objNum)
// objNum.a = 1;
console.log(typeof objNum)
// console.log(objNum.a)
// console.log(objNum)

  把上面的代码,复制到你的现代浏览器里,就会发现,实际上,objNum是一个对象,而我们通过字面量所创建的数字,本质上,也是通过上面的方法创建的。所以,值类型,其实也是对象,只是它被隐藏起来了罢了。

  那么函数呢?typeof的结果里不是还有个function么?其实函数也是对象。

  注意:这里有一个问题,就是值类型到底算不算是对象!首先,我觉得值类型也算是对象的。原因上面说过了,但是这里有一个问题就是,通过字面量创建的值类型,它的表现形式确实不是对象,而且也无法添加属性。那么,这里我猜测,为了便于开发者使用,通过字面量创建的值类型,经过了一定的转换。所以,并不是值类型不是对象,而是通过字面量创建的值类型,抛除了一部分对象的特性,使其只专注于自身的“值”。(以上纯属个人理解)

 

四、函数

  上一小节我说了,对象是对象,值也是对象,在结尾的时候又说了,函数也是对象?

var fun = function () { };
var fun1 = function () { };
console.log(fun === fun1)
// fun.a = 1;
// fun.b = function () {
//     console.log(this.a)
// }
// console.log(fun.a)
// fun.b()

  其实上面的代码我偷懒了,但是我觉得你们看得懂。不懂的话...就...留言吧。(其实就是注释啦)。

  通过以上的代码,我们发现,fun具有唯一性,两个空函数是不相等的,且可以拥有属性(a)行为(b),所以,这绝壁是一个对象啊。没毛病!

  之前在对象的部分,我们给对象做了简单的分类。那么实际上,函数也是有各种不同的分类的。为什么呢?其实这里可以理解的很简单:对象是如何产生的?理论上讲,对象是通过函数,也即构造函数创建的(当然有一些原生对象是JS生成的),无论我们以何种形式得到的对象,本质都是如此。即便var obj = {};这样的代码,实际上也是var obj = new Object()生成的。所以,对象有不同的种类,函数其实也有,通过对象的分类,可以简单推算出(这里是我结合ECMAScript标准和对象的分类整理的):函数只有内置函数(其实这里说,只有内置函数是片面的,但是方向是没问题的,其实还有一些比如bound function,strict function,但是我觉得这些实际上并不完全的属于一个独立的分类或者体系,它更像是内置函数的一个子集,所以我们这里简单理解成内置函数就可以了,比如我们自己通过字面量创建的函数,实际上也是通过new Function得到的函数)。

  内置函数大概有以下几种:Number、Date、String、Boolean、Object、Function、Error、Array等常用的八种。还有Global不能直接访问,Arguments仅在函数调用时由JS引擎创建,Math和JSON是以对象的形式存在的。

  这么多构造器可以创建对象,我怎么知道它是由谁创建的?我怎么知道我是谁呢?typeof在此刻好像就不那么灵光了。

 

五、我是谁之instanceof

  之前说了,内置构造器有很多种,那么我怎么区分“我是谁”呢?这时instanceof就派上用场了。instanceof的作用就是:判断a 与 b的原型上游,是否存在指向的相同的引用(假设是a instanceof b,也就是分别判断a.__proto__和b.prototype上游)。isntanceof不仅仅可以使用在实例与构造函数之间,也可以用在父类与子类之间(反正就是判断a、b能否在原型链上找到同一个引用)。

function Person() { };
var p = new Person();
console.log(p instanceof Person)
function Zaking() { };
Zaking.prototype = p;
var z = new Zaking();
console.log(z instanceof Person)

  

六、函数与对象间关系

  前面说了基本的数据类型、对象、函数等的分类,下面我们就来详细的说一下函数与对象间的关系,我们先来看一个简单的代码:

function Person() { };
var p = new Person();

  就是这样,很简单,我们创建一个函数(构造函数),然后生成一个对应的实例(对象)。那他俩之间有什么关系呢?又是如何体现的呢?

  我们可以通过constructor属性,来判断:

console.log(p.constructor === Person) //true

  我们发现实例对象p的构造函数指针正是Person,但是有一个奇怪的地方,就是:

console.log(p.hasOwnProperty('constructor')) //false

  就是,p本身并没有constructor属性,那这个constructor是从哪来的呢?

 

七、prototype

  我们先暂时忘记实例上的constructor是从哪来的这个问题。我们先来看一下prototype这个东西。

  在此之前,我们先要了解另外一种对象的分类方式,即把对象分为函数对象普通对象,那这样分类的依据是什么呢?从规范上来说,即该对象是否拥有call方法,从表象一点的方向来看,可以用typeof的结果是function还是object来区分。typeof的结果是function的就是函数对象,typeof结果是object,就是普通对象。

  我们之前说过了,函数也是一种对象,所以函数本身也是有一些属性和方法的,而JavaScript自己就给函数对象添加了一些属性,其中就有prototype。每一个函数对象都有prototype原型对象。

console.log(Person.prototype)

  打印的结果是这样的:

 

  唉?这里有个constructor属性,它指向了Person自己?是的

console.log(Person.prototype.constructor === Person);//true

  那结合之前的代码p.constructor === Person,不就是说:

console.log(Person.prototype.constructor === p.constructor);// true

  没错,我们此时,找到了对象(实例)与函数(构造函数)之间的关系了!

 

八、__proto__

  上面一小节,我们验证了对象与函数间的关系,但是仍旧遗留了一个问题,就是实例p本身并没有constructor属性,那它是从哪来的呢?这就不得不说一下,__proto__这个东西了,它叫做隐式原型。每一个对象都有一个__proto__隐式原型(原型对象,也是对象,所以它也有__proto__,即A.prototype.__proto__)(但是__proto__并不是规范,它只是浏览器的实现而已)

  那,之前说过实例p没有constructor属性,那p的__proto__是不是可能会有constructor呢?我们猜测一下呗?

console.log(p.__proto__.hasOwnProperty('constructor')); //true

  唉?它是在实例的隐式原型上的,没问题!那这样的话,是不是说...

console.log(p.__proto__ === Person.prototype);//true

  没错!就是这样的,实例的隐式原型和构造函数的原型是相等的,指向同一个指针的!

 

九、原型链

  上一小节,我们初步的看到了原型与隐式原型间的关系,实际上,这就是原型链的初步形成。但是,我相信大家想知道的肯定不单单是这些。嗯...当然。我们下面就一点点剖析。

  通过之前的代码,我们知道了实例的隐式原型是等于构造函数的原型的。那之前又说过,构造函数的原型也是一个对象,那它也有隐式原型:

console.log(Person.prototype.__proto__)

  没错,但是这里首先有一个问题,就是Person.prototype是什么?其实它就是一个对象啊。所以它才有__proto__啊。那Person.prototype.__proto__的结果是什么呢?

我猜是Object.prototype:

console.log(Person.prototype.__proto__ === Object.prototype); // true

  那依此类推,Object.prototype也有__proto__啊。

console.log(Object.prototype.__proto__); // null

  唉?null?是的,到这里实际上,Object.prototype就没有隐式原型了,因为到顶了。

  ok,到这里我们原型链第一阶段的问题已经解决了,下面我们开始第二阶段的问题。

  还记不记得我之前说过,函数对象拥有prototype原型, 每一个对象都拥有__proto__隐式原型,所以!函数对象,也是对象!也有__proto__隐式原型。即:

console.log(Person.__proto__);

  那Person.__proto__又是从哪来的呢?那根据前面第一阶段的代码,假设,Person是一个对象,那它肯定是由某个构造函数创建出来的,那在js中是谁创建出一个Person函数的呢?换句话说,我们在function Person(){}的时候,实际上Person是这样创建的var Person = new Function()。(注意!绝对不推荐这样创建函数,这里只是演示它从哪来的。)

  哦吼?原来是这样,那也就是说。

console.log(Person.__proto__ === Function.prototype); // true

  那Function.prototype也是一个对象。也就是说:

console.log(Function.prototype.__proto__ === Object.prototype); // true

  到了这里,是不是有点破案的味道了?

  到此就结束了么?没有,其实到这里我们才刚开始。

  先简单总结一下,刚开始,我们从一个对象的角度(即构造函数生成的实例),然后我们又从函数的角度(即构造函数)分为两条线来捋了一下,其实这就是原型链了,只是function和object互相的关系有点烦人罢了。

  之前说过,函数有几种内置构造函数,也可以称之为包装函数,大概我们可以用的有那么几种Number、Date、String、Boolean、Object、Function、Error、Array。一些比如:Number、Date、String、Boolean、Error、Array,Regex这些,对应的对象都是由这些包装函数生成的。其实,我们可以把这些(Number、Date、String、Boolean、Error、Array)在原型链的概念中,当作是我们自己通过Function创建的Person构造函数。因为它们在这里的体现形式和作用、使用方式都是一模一样的。不信你看:

console.log(Person.__proto__ === Function.prototype);//true
console.log(Number.__proto__ === Function.prototype);//true
console.log(String.__proto__ === Function.prototype);//true
console.log(Boolean.__proto__ === Function.prototype);//true
console.log(Date.__proto__ === Function.prototype);//true
console.log(Array.__proto__ === Function.prototype);//true
console.log(Error.__proto__ === Function.prototype);//true
console.log(RegExp.__proto__ === Function.prototype);//true

  毫无疑问,都是true。并且,你再看:

console.log(Function.__proto__ === Function.prototype);//true
console.log(Object.__proto__ === Function.prototype);//true

  因为Function和Object在这里都是作为包装函数所出现的,所以,它们必然都是由Function创建的(在这里,不要多想,把Function和Object都当作包装函数就好了)。所以,我们可以得到:所有的构造函数的__proto__都指向Function.prototype,Function.prototype是一个空函数(自己去console)。Function.prototype也是唯一一个typeof结果为function的prototype(特殊记忆一下,其实如果抛去对象的话,说Function是万物之源也没错,记住,是抛除Object的话!)。其他所有的构造器的prototype都是object。

  相信到了这里,大家有一丝丝的顿悟,但是又有些混乱。混乱的主要原因就在于Object和Function这两个东西(构造函数)。因为它们本身即作为构造函数出现,又作为对象出现。导致它们之间有循环调用的存在。

console.log(Function.__proto__ === Function.prototype);//true
console.log(Object.__proto__ === Function.prototype);//true
console.log(Function.prototype.__proto__ === Object.prototype);//true

  我从之前的代码中,摘出了这三句。当Function和Object都作为“对象”时,它们的隐式原型都指向Function.prototype,没问题,因为它们都是作为构造函数存在的“函数对象”。而,这里Function.prototype本质上又是一个对象,所以它的隐式原型Function.prototype.__proto__就指向了Object.prototype。也没问题。然后就是Object.prototype.__proto__ === null就结束到顶了。

 

十、总结

  其实到这里,原型链的部分就结束了。我们来复习,整理一下之前我们说过的内容。

  首先,我们聊了js的数据类型,分为规范类型语言类型,规范类型有8种,主要用于标准的描述和内部的实现,语言类型也有8种(Number、String、Boolean、Undefined、Null、BIgInt、Symbol、Object)。

  然后,通过typeof 运算符对于Object运算时产生的不同结果,引出了对象和函数。并对对象和函数都做了类别的区分。

  再然后,通过简单描述对象与函数间关系,我们引出了Object和Function之间复杂的原型关系。

  最后,我们分别讲了prototype和__proto__,然后我们聊了下__proto__和prototype之间的关系。

  然后这里要强调几个关键点:

  1. 对象可以分为“函数对象”和“普通对象”,函数对象就是函数,它在js中也算是一种对象,可以拥有行为和属性,并且具有唯一性。普通对象就是通过new 构造函数或字面量等方法创建的对象。
  2. 只有函数对象拥有prototype原型对象,但是所有的对象(当然就是函数对象和普通对象)都有__proto__隐式原型。
  3. Object和Function这两个构造器比较特殊,由于它们本身是函数对象,所以即拥有prototype又拥有__proto__。所以,实际上,一切复杂的源头都在这里。

 

十一、现代原型操作方法

  实际上,__proto__这个东西,在现代标准(指最新的提案或已纳入标准的某个ES版本,具体哪个版本的好难找,就先这么叫吧)中,已经不推荐使用。那我们如何操作原型呢?

下面,我们就学习一下操作或涉及到原型的一些方法:

1、Object.prototype.hasOwnProperty,(现在知道obj.hasOwnProperty这样的用法从哪来的了吧),方法会返回一个布尔值,指示对象自身属性(即不是从自己或祖先的原型中存在的)中是否具有指定的属性(也就是,是否有指定的键)。

const object1 = {};
object1.property1 = 42;
console.log(object1.hasOwnProperty('property1')); // true
console.log(object1.hasOwnProperty('toString')); // false
console.log(object1.hasOwnProperty('hasOwnProperty')); // false

console.log(object1.__proto__.hasOwnProperty('toString')); // true
console.log(object1.__proto__.hasOwnProperty('hasOwnProperty')); // true

console.log(Object.prototype.hasOwnProperty('toString')); // true
console.log(Object.prototype.hasOwnProperty('hasOwnProperty')); // true

 

2、Object.prototype.isPrototypeOf(),用来检测一个对象是否存在于另一个对象的原型链上。

var obj = {}
console.log(Object.prototype.isPrototypeOf(obj))
// 类似于
console.log(Object.prototype === obj.__proto__)

  实际上,它就相当于是通过恒等运算符来判断下obj的隐式原型是否和构造函数Object的原型指向同一个指针,当然,这只是一个简单的例子,如果我们自定义某一个原型的指向也是可以的:

function Foo() { }
function Bar() { }
function Baz() { }

Bar.prototype = Object.create(Foo.prototype);
Baz.prototype
= Object.create(Bar.prototype); var baz = new Baz(); console.log(Baz.prototype.isPrototypeOf(baz)); // true console.log(Bar.prototype.isPrototypeOf(baz)); // true console.log(Foo.prototype.isPrototypeOf(baz)); // true console.log(Object.prototype.isPrototypeOf(baz)); // true

// 上面的代码实际上相当于这样:

  哦对,在开始解释、类比之前,还得多说一句,就是Object.create(后面有其实)是干啥玩意的的。其实看一段代码你就知道了:

...这里承接上面的代码哦...
console.log(new Foo().constructor === Object.create(Foo.prototype).constructor)

  你猜结果是啥,是true呗,其实Object.create就是new Foo()。那,我们再来看个有意思的...算了,这里不多说了,不是地方,具体到Object.create的时候再说吧。这里你只需要记住上面的console结果的意义就可以了。

  那么,我们继续按照传统的、我们前面学过的那种方式来重写一下这些代码:

    function Foo() { }
    function Bar() { }
    function Baz() { }

    Bar.prototype =  new Foo();
    Baz.prototype =  new Bar();
    var baz = new Baz();
    console.log(Baz.prototype === baz.__proto__); // true
    console.log(Bar.prototype === baz.__proto__); // false
    console.log(Foo.prototype === baz.__proto__); // false
    console.log(Object.prototype === baz.__proto__); // false

    // 唉?咋不对,咋跟之前的代码结果不一样?你不是说了类似的么?
    // 我们再来看这句话:用来检测一个对象是否存在于另一个对象的原型链上。
    // 看!是原型链上!!!不是这个对象的原型上。

    // 所以,最开始的代码可以是这样的
    console.log(Baz.prototype === baz.__proto__);
    console.log(Bar.prototype === baz.__proto__.__proto__);
    console.log(Foo.prototype === baz.__proto__.__proto__.__proto__);
    console.log(Object.prototype === baz.__proto__.__proto__.__proto__.__proto__);

  唉?我擦嘞,好像有点意思诶?其实我觉得到这里,大家就都懂了。但是我还是说一下吧,我们仍旧来看代码:

    // 我们先从头看,要注意,这里的头,是从var baz = new Baz();开始的
    var baz = new Baz();
    // 这句话所导致的结果就是
    console.log(Baz.prototype === baz.__proto__);
    // 这毋庸置疑的对吧。
    // 那我们看下一句:
    Baz.prototype =  new Bar();
    // 实际上这句话的意思就是
    var bar = new Bar();
    // 所以
    console.log(Bar.prototype === bar.__proto__);
    Baz.prototype = bar;
    // 我觉得到这里差不多你就懂了
    // 我们继续,同上
    Bar.prototype =  new Foo();
    // 也即
    var foo = new Foo();
    console.log(Foo.prototype === foo.__proto__);
    Bar.prototype =  foo;

    // 其实后面还可以有点,但是我不想说了,再不懂,我真的伤心了,再有,你自己写下Object的那个console是怎么来的吧。

  好了,说的有点多了,我们看下一个吧。

3、Object.getOwnPropertyNames(),方法返回一个由指定对象的所有自身属性(不包括原型链上)的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。换句话说,它会返回除Symbol作为key的所有自身属性的数组。

let sym = Symbol('sym');
var obj = {
    a: 1,
    b: 2,
    c: { a: 1 }
}
obj[sym] = 1
Object.defineProperty(obj, 'p1', {
    value: 42,
    writable: false,
    enumerable: false
});
Object.prototype.m = 'abcd';
obj.__proto__.n = '1234';
console.log(obj[sym]);
console.log(Object.getOwnPropertyNames(obj));

 

4、Object.getOwnPropertySymbols(),方法返回一个给定对象自身的所有 Symbol 属性的数组。上一个不能返回symbol的,这回这个只能返回symbol的。

let sym = Symbol('sym');
var obj = {
    a: 1,
    b: 2,
    c: { a: 1 }
}
obj[sym] = 1
Object.defineProperty(obj, 'p1', {
    value: 42,
    writable: false,
    enumerable: false
});
console.log(obj[sym])
console.log(Object.getOwnPropertySymbols(obj))

 

5、Object.getPrototypeOf(),该方法返回指定对象的原型的值。其实个人理解,就相当于Object.prototype,或者obj.__proto__。

console.log(Object.getPrototypeOf({}))
console.log(Object.getPrototypeOf({}) === {}.__proto__)
console.log(Object.getPrototypeOf({}) === Object.prototype)

  还有这:

var num = new Number();
console.log(Object.getPrototypeOf(num))
console.log(Object.getPrototypeOf(num) === num.__proto__)
console.log(Object.getPrototypeOf(num) === Number.prototype)

  这个挺简单的,我感觉没啥问题,就不多说了。

 

6、Object.setPrototypeOf(),方法设置一个指定的对象的原型到另一个对象或null。应尽量避免更改原型的属性,而是使用Object.create创建一个拥有你需要的原型属性的对象。

  如果对象的原型属性被修改成不可扩展(通过 Object.isExtensible()查看),就会抛出 TypeError异常。如果prototype参数不是一个对象或者null(例如,数字,字符串,boolean,或者 undefined),则什么都不做。否则,该方法将obj原型修改为新的值。

var obj1 = {};
var obj2 = {};
Object.setPrototypeOf(obj1, { m: 1 })
console.log(obj1.__proto__)
console.log(Object.prototype)
console.log(Object.prototype === obj1.__proto__)
console.log(obj2.__proto__)
console.log(Object.prototype === obj2.__proto__)

  要注意的是,只是“设置”了某个指定的对象的原型,而不是更改了整个原型链,很好理解吧。其实说是改变了原型链也行,因为若是上游的原型变了,下游的原型链自然也就变了。

  其实上面的代码等同于:

var obj3 = new Object();
var obj4 = new Object();
obj3.__proto__ = {
    m : 3
};
console.log(obj3.__proto__)
console.log(Object.prototype)
console.log(Object.prototype === obj3.__proto__)
console.log(obj4.__proto__)
console.log(Object.prototype === obj4.__proto__)


// 所以我们可以知道,本质上,Object.prototype它们指向的对象内容相同,但是不代表它们指向的是同一个对象。
// 所以,我们更改了对象的原型后,自然就不想等了。
// 那要是想要想等怎么办?其实也不难,方法有很多。
// 比如:
Object.prototype = {z:9};
var a = new Object();
console.log(Object.prototype === a.__proto__);
console.log(Object.prototype);
console.log(a.__proto__)
// 其实,这算是继承啦,不多说。

 

7、Object.create(),方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。换句话说,就是给新创建的对象指定其原型对象。

var obj = { a: 1 };
var objx = Object.create(obj);
console.log(objx.__proto__);

var objy = { a: 2 };
var objz = {};
objz.__proto__ = objy;
console.log(objz.__proto__);

var objm = Object.create(null);
console.log(objm.__proto__ === Object.prototype)

  该方法还有第二个可选参数。详情可于MDN查看。

8、Object.assign(),方法用于将所有自身可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。注意,是所有可枚举属性,包括Symbol,但不包括原型上的属性。

let sym = Symbol('sym');
var obj = {
    a: 1,
    b: 2,
    c: { a: 1 }
}
obj[sym] = 1
Object.defineProperty(obj, 'p1', {
    value: 42,
    writable: false,
    enumerable: false
});
obj.__proto__.m = 1;
var a = Object.assign({}, obj)
console.log(a)

 

9、Object.keys(),方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致。Symbol无法被枚举出来。

let sym = Symbol('sym');
var obj = {
    a: 1,
    b: 2,
    c: { a: 1 }
}
obj[sym] = 1
Object.defineProperty(obj, 'p1', {
    value: 42,
    writable: false,
    enumerable: false
});
obj.__proto__.m = 1;
var a = Object.assign({}, obj)
console.log(Object.keys(a), '---')

 

10、Object.values(),方法返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用for...in循环的顺序相同(区别在于 for-in 循环会把原型链中的属性也枚举出来)。

let sym = Symbol('sym');
var obj = {
    a: 1,
    b: 2,
    c: { a: 1 }
}
obj[sym] = 1
Object.defineProperty(obj, 'p1', {
    value: 42,
    writable: false,
    enumerable: false
});
obj.__proto__.m = 1;
var a = Object.assign({}, obj)
console.log(Object.values(a), '---')
for (var k in obj) {
    console.log(k, '--k--')
}

 

11、Object.entries(),方法返回一个给定对象自身可枚举属性的键值对数组。

let sym = Symbol('sym');
var obj = {
    a: 1,
    b: 2,
    c: { a: 1 }
}
obj[sym] = 1
Object.defineProperty(obj, 'p1', {
    value: 42,
    writable: false,
    enumerable: false
});
obj.__proto__.m = 1;
console.log(Object.entries(a))

 

12、Object.is(),该方法判断两个值是否为同一个值。这个好像没啥好说的,但是想要说的又很多。去看MDN吧。

13、Object.seal(),方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。

14、Object.freeze(),方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

15、Object.preventExtensions(),方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。

16、Object.isExtensible(),方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。

17、Object.isFrozen(),方法判断一个对象是否被冻结。

18、Object.isSealed(),方法判断一个对象是否被密封。

19、Object.fromEntries(), 方法把键值对列表转换为一个对象。

const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
]);

const obj = Object.fromEntries(entries);

console.log(obj);

 

20、Object.defineProperty(),方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。注意:应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改。

var objc = {};

Object.defineProperty(objc, 'property1', {
    value: 42
});

objc.property1 = 77;
// throws an error in strict mode

console.log(objc.property1);
// expected output: 42

 

21、Object.defineProperties(),方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。该方法可以定义多个属性。

var obj = {};
Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  }
  // etc. etc.
});

 

22、Object.getOwnPropertyDescriptor(),方法返回指定对象上一个自有属性对应的属性描述符。

var objc = {};

Object.defineProperty(objc, 'property1', {
    value: 42
});

objc.property1 = 77;
// throws an error in strict mode

console.log(objc.property1);
// expected output: 42

var desc1 = Object.getOwnPropertyDescriptor(objc, 'property1');
console.log(desc1.configurable)
console.log(desc1.writable)
console.log(desc1)

 

23、Object.getOwnPropertyDescriptors(),方法用来获取一个对象的所有自身属性的描述符。所指定对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。

  Object.assign() 方法只能拷贝源对象的可枚举的自身属性,同时拷贝时无法拷贝属性的特性们,而且访问器属性会被转换成数据属性,也无法拷贝源对象的原型,该方法配合 Object.create() 方法可以实现上面说的这些。

Object.create(
  Object.getPrototypeOf(obj), 
  Object.getOwnPropertyDescriptors(obj) 
);

 

24、Object.prototype.toString(),每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中 type 是对象的类型。以下代码说明了这一点:

var o = new Object();
console.log(o.toString()); // returns [object Object]
console.log(Object.prototype.toString.call(new Array())); // returns [object Array]

 

25、Object.prototype.valueOf(),这个东西,用到的不多。自己去看吧

// Array:返回数组对象本身
var array = ["ABC", true, 12, -5];
console.log(array.valueOf() === array);   // true

// Date:当前时间距1970年1月1日午夜的毫秒数
var date = new Date(2013, 7, 18, 23, 11, 59, 230);
console.log(date.valueOf());   // 1376838719230

// Number:返回数字值
var num =  15.26540;
console.log(num.valueOf());   // 15.2654

// 布尔:返回布尔值true或false
var bool = true;
console.log(bool.valueOf() === bool);   // true

   

十二、课后作业之原型小练习

 

 1、以下代码中的p.constructor的constructor属性是从哪来的?Person.prototype.constructor呢?

function Person() { };
var p = new Person();
console.log(p.constructor);
console.log(Person.prototype.constructor);

答:首先p.constructor是p.__proto__中来的。

console.log(p.constructor === p.__proto__.constructor);
 console.log(p.__proto__.hasOwnProperty('constructor'));

  其次,Person.prototype.constructor,是Person.prototype自身的。

console.log(Person.prototype.hasOwnProperty('constructor'))

 

2、typeof Function.prototype的结果是什么?typeof Object.prototype的结果又是什么?

console.log(typeof Function.prototype);
console.log(typeof Object.prototype);

答:typeof Function.prototype的结果是function,typeof Object.prototype是object。Function.prototype比较特殊,因为它的typeof 结果说明它是函数对象,但是它本身又是没有prototype属性的。也就是说

console.log(Function.prototype.prototype); // undefined

  那,为什么Function.prototype会是一个函数对象呢?解释是:为了兼容以前的旧代码...好吧。这就是为什么会有奇葩的:Function.prototype是一个函数,但是Function.prototype的隐式原型又是Object.prototype。即:

console.log(Function.prototype.__proto__ === Object.prototype); // true

3、Object.prototype?Object.__proto__?Function.prototype?Function.__proto__?

答:

console.log({}.__proto__ === Object.prototype);
console.log(Object.__proto__ === Function.prototype);
console.log(Function.__proto__ === Function.prototype);
console.log(Function.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__ === null);

  其实这里唯一无法理解的是,Function.prototype是一个空函数;

console.log(Function.prototype);

  换句话说,Function.prototype是一个函数对象。之前说过,函数对象的原型都是Function.prototype。但是实际上特殊的Function.prototype的原型却是Object。原因,就是为了兼容旧代码。所以这里特殊记忆一下吧。

 

  最后,这篇文章到这里就基本上结束了,回过头来看发现原型的概念似乎并不复杂,也确实如此。复杂的是变化的场景,但是万变不离其宗。还有一些特殊的情况,可能是历史原因,也可能是为了兼容,不管怎么样,这种特殊情况就特殊的记忆一下就好了。

  其实最开始写这篇文章是忐忑的,我看了一些文章,总觉得对原型链的描述不够清晰详尽,恰好自己最近也在学习一些js的深入内容。所以,就想当作是整理自己的学习思路,来造一造轮子。但是通篇下来,也还是没有达到我想要的满意的程度。一些逻辑的承接,一些细节的深入也都还是不够。

  最后,希望这篇文章能给你带来些许的收获,也希望你发现了什么不解或者疑问可以留言交流。

   

  其实个人觉得这里有点问题的地方在于MDN中摘抄的现代原型操作方法,由于这些并不属于本章核心内容,所以我只是做了简单的摘抄和潦草的分析,如果大家有兴趣,可以自己去学一下,后面我也会写一篇关于继承的相关文章。一定会包括这些内容。实际上在本篇内容里,多多少少都带了一些“继承”,没办法,谁让原型和继承,本身就很难分割呢。

 

本文参考及借鉴:

  1. 最详尽的 JS 原型与原型链终极详解,没有「可能是」——Yi罐可乐
  2. 深入理解javascript原型和闭包(完结)《原型部分》——王福朋
  3. ECMAScript® 2018 Language Specification 
  4. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object 

 

  感谢以上作者的文章,学以致用,道阻且长。

 

 

附、补充(文中未表述清晰或遗漏的重要内容):

  1、值类型到底是对象么?(更正第三小节、三点三小节对应内容)

  答:其实通过包装函数创建的值类型是对象!这点毋庸置疑。但是通过字面量创建的值类型是对象么?如果是,那它可以添加属性么?如果不是,为什么可以使用原型链上的方法比如1..toString()(没写错,1..toString())呢?实际上,通过字面量创建的值类型并不能完全的称之为“对象”。因为它没有属性和行为,也不唯一。但是它却可以使用原型链上的方法,究其原因,是因为在js运行时给值类型做了一层包装,使其可以使用原型链上的方法。而并不是因为值类型本身就是对象。

  2、我总觉得这篇文章还差点什么,不够我想要的那种感觉,我其实想要在文章做到由浅入深,但是整理后发现,浅是浅了,浅着浅着就发现浮上来来,一点都不深了。所以,为了不破坏文章的结构和思路(思路是没问题的),翻来覆去之后,还是把这张图贴上来了。

  想必这张图在大家学习原型链的过程中一定见过不少次,不得不说,这也确实是我觉得最为贴切详细的一张图。所以,最后的最后,我们还是用代码来仔细的过一遍这张图吧。

function Foo() {};
var f1 = new Foo();
var f2 = new Foo();
var o1 = new Object();
var o2 = new Object();
console.log(f1.__proto__ === Foo.prototype);
console.log(Foo.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__ === null);
// 这条线到这里就完事了!
// 我们来看第二条
console.log(o1.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__ === null);
// 这个也完事了

// 我们再从另外一个角度来看
console.log(Foo.__proto__ === Function.prototype);
console.log(Function.prototype.__proto__ === Object.prototype);// 又来了,下面的不写了

// 所以,同理
console.log(Object.__proto__ === Function.prototype);
// 所以,又来了
console.log(Function.prototype.__proto__ === Object.prototype);

// 还有一个
console.log(Function.__proto__ === Function.prototype); // 后面不写了

// 最后
console.log(Object.prototype.constructor === Object);
console.log(Function.prototype.constructor === Function);
console.log(Foo.prototype.constructor === Foo);

  那么我们来看几个小问题吧:

console.log(typeof Function.prototype)
console.log(typeof Function.__proto__)
console.log(Function.__proto__)
console.log(Function.prototype)
console.log(typeof Object.prototype)
console.log(Object.__proto__ === Function.prototype)

  最后的最后,其实原型链的精髓就是:

  • o1.__proto__ === Object.prototype
  • o1.__proto__.constructor === Object.prototype.constructor
  • Object.__proto === Function.prototype
posted @ 2020-08-17 15:40  Zaking  阅读(596)  评论(0编辑  收藏  举报