理解对象(或者说函数的)的原型(能够经过 Object.getPrototypeOf(obj) 或者已被弃用的 proto 属性得到)与构造函数的prototype属性之间的区别是很重要的。前者是每一个实例上都有的属性,后者是构造函数的属性。也就是说,Object.getPrototypeOf(new Foobar())和Foobar.prototype指向着同一个对象。javascript
这个__proto__
是[[Prototype]]
的因历史缘由而留下来的 getter/setter(一个getter函数和一个setter函数), 暴露了经过它访问的对象的内部[[Prototype]]
。__proto__
这个东西已经从web标准中废弃了,尽管在Chrome中还可使用,能够经过它修改一个对象的[[Prototype]]
属性,可是这是很是耗性能的。同时,原型链中的方法和属性没有被复制到其余对象——它们被访问须要经过前面所说的“原型链”的方式。没有官方的方法用于直接访问一个对象的原型对象——原型链中的“链接”被定义在一个内部属性中,在 JavaScript 语言标准中用
[[prototype]]
表示(参见 ECMAScript)。然而,大多数现代浏览器仍是提供了一个名为__proto__
(先后各有2个下划线)的属性,其包含了对象的原型。php
每次讲到原型,我都要回顾下下面这张图:html
原型既指构造函数的prototype属性指向的对象前端
其实也指实例的
[[Prototype]]
属性(经过__proto__访问),它们指向的是同一个东西。
参考:JavaScript学习笔记(十二) 原型 文中的图片不错,很形象。java
原型是 function 对象的一个属性,见上图Person.prototype
,原型定义了构造函数制造出的对象的公共祖先。经过该构造函数产生的对象,能够继承该原型的属性和方法,原型也是对象。node
天然而然我联想到了Java中的继承,进而Java有了重写,一样JS咱们能够在对象中对原型的方法和属性进行重写,可是不能经过修改对象的属性修改(此处指增删改查)原型,想要修改原型只有把原型调出来才能修改,以下代码:
Person.prototype.age = 22; //直接调出来修改 Person.prototype.name = "田"; function Person(age, name) { this.age = age; this.name = name; } var per = new Person(23, "挥动"); console.log(per);
这里题外话解释一下JavaScript构造函数内部原理web
this = {}
(这一步存在疑问,下面解释)以下算法
Person.prototype.age = 22; function Person() {} var per = new Person(); console.log(per.age); //22
Person()
是一个构造函数,而.prototype
是系统在 Person
出生时添加的属性,prototype
译为原型,原型是构造函数构造出的对象的公共祖先,若是不对它作什么修改,那么原型值 = {}
,此处咱们加了age = 22
数组
此处附加一个小知识,图中浅粉色表明系统给你写的,紫色表明你本身写的,见代码,构造函数是系统写的,而age是你本身在原型中加的,系统给定的构造函数咱们也能够本身在代码中调出原型进行修改,如上图
如今回到上面存疑的步骤,开篇讲过构造函数的原理第一步this = {}
,值得怀疑的是他到底传入的是否是空的{}
, 浏览器
在控制台查看它并非空的,里面有一个__proto__
属性,那么第一步实际上应该是下面这样
// 注意这是伪代码 var this = { __proto__:Person.prototype }; // 能够在控制台中试一试:per.__proto__===Person.prototype返回true
当咱们在查找对象的属性或方法时会首先在本身里面找,若是找不到会找__proto__
指向的原型,这样就把对象和原型链接到了一块儿。__proto__
存的是对象的原型,或者换句话说每一个对象都有一个__proto__
指向构造它的原型,由此咱们能够发现 __proto__
指向的构造函数是能够修改的,所以per的构造函数也就未必是Person()
了,也就是说Person()
这个构造函数构造出来的对象的原型不必定是Person.prototype
,以下图
咱们再来看下面的代码结果是这样的,为何?
function Person() {} var per1, per2, per3; per1 = new Person(); console.log(per1); //Person {} Person.prototype.name = "sunny"; //经过结果发现,影响到了per1 per2 = new Person(); Person.prototype = { //这个却没有影响到per1和per2,为何? name: "cherry" } per3 = new Person(); console.log("per1.name:" + per1.name); //per1.name:sunny console.log(per1.__proto__); //{name: "sunny", constructor: ƒ} console.log("per2.name:" + per2.name); //per2.name:sunny console.log(per2.__proto__); //{name: "sunny", constructor: ƒ} console.log("per3.name:" + per3.name); //per3.name:cherry console.log(per3.__proto__); //{name: "cherry"} console.log(per1.__proto__ === per2.__proto__); //true console.log(per2.__proto__ === per3.__proto__); //false console.log(per3.__proto__.__proto__ === Object.prototype); //true
咱们讲到构造函数第一步至关于var this = {__proto__:Person.prototype}
,在这里__proto__
和Person.prototype
都至关于指向了同一个空间,per1建立的时候构造函数里面什么都没有写,尽管per1的__proto__
并无修改,可是它和per2指向的是同一个空间,咱们在后面修改了Person.prototype
的值,因此后面per二、per1的输出值都是sunny。
对于per3咱们发现它没有构造函数,由于咱们后面修改原型的方式不是修改Person.prototype
所指向的对象中的一个属性,而是给Person.prototype
直接赋值一个新的对象,因此per3输出cherry,而且这个新的对象是以对象字面量的方式建立的,默认这种对象的原型就是Object,因此per3.__proto__.__proto__ ===Object.prototype //true
。
let obj = { name: "HUI" }; console.log(obj.__proto__ == Object.prototype); //true
关于预编译能够参考:JavaScript预编译、做用域
function Grand() { this.word = "我是爷"; } var grand = new Grand(); Father.prototype = grand; //grand是用Grand()建立的一个对象实例,它是个对象 function Father() { this.name = "我是爹"; } var father = new Father(); Son.prototype = father; // 同上,直接给原型赋值一个对象 function Son() { this.hobbit = "smoke"; } var son = new Son();
像这样在原型上面再加一个原型对象的方法叫作原型链,原型链使用__proto__
来链接各个原型,控制台尝试输出如图
在其中能够看出,Grand的__proto__
指向Object,而再点开Object发现已经没有__proto__
属性(有prototype),说明Object()
就是原型链的终端(实际上Object.prototype.__proto__
指向null,也能够说null是终端)。
只能在本身身上经过原型删除(delete father.name
),不能经过父代或者后代来删除
能够本身增,后代通常不能增。可是不排除下面这种状况:在后代中 增长 了父级对象的属性。经过子代调用了引用 修改或者说增长了父代的属性,这是一种调用的修改而不是赋值的修改,而且这种修改也仅限于引用值,好比原始值你就只能覆盖,不能修改。
function Father() { this.name = "我是爹"; this.for = { sd1: "哈哈哈", } } var father = new Father(); Son.prototype = father; // 同上,直接给原型赋值一个对象 function Son() {} var son = new Son(); console.log(son); son.for.sd2 = "操"; son.name = "更名"; //最终father没有更名,而是在son新增了名字 console.log(father); son.father.name = "更名"; //Uncaught TypeError: Cannot set property 'name' of undefined
Father.prototype = { num: 100 } function Father() { this.eat = function () { this.num++; //把值拿过来+1再赋给本身 } } var son = new Father(); son.eat(); console.log(son.num); //101 console.log(Father.prototype.num); //100
还需注意原型是隐式的内部属性,只有系统给咱们的才能用,假如咱们本身在一个没有原型的对象中添加了__proto__
属性,系统是不能识别的。
var obj = Object.create(null); obj.__proto__ = { name: "sunny" } console.log(obj); //{__proto__:{name: "sunny",__proto__: Object}} console.log(obj.name); //undefined 此时发现从原型链上找是找不到的
注意图中,obj.__proto__
是能够访问的,由于咱们赋值的时候是一个对象{name: "sunny"}
,可是Chrome并不能识别obj.name
,此时的obj并非继承自Object.prototype
,由此总结绝大多数对象最终都会继承自Object.prototype
,但不是所有,还需注意Object.create(原型)
方法必须传入object或者null!不能为空。
注意:咱们发现toString()
方法在Object
的原型中,那么应该说不少通过包装类的值均可以使用这个方法,不过 undefined、null 以及本身构造的没有原型的对象是没有这个方法的。
值得注意的是许多东西均可以调用toString()
方法,按我所想调用的都是Object原型中的方法,可是事实并不是如此,举个例子
Object.prototype.toString = function () { return "人为重写"; //在此重写Object的toString方法 } var nun = 123; console.log(nun.toString()); //123,明显nun调用了重写的方法(若是有的话) console.log(Number.prototype.toString.call(nun)); //123 console.log(Object.prototype.toString.call(nun)); //人为重写
方法的重写不但出现于工程师与机器之间,也存在于机器和本身之间,nun.toString()
其实是调用了重写的方法,为何?由于Object的toString方法实际上不完善,输出的信息没什么用,假如调用Object的方法就会输出"[object Number]"
,因此须要重写方法,诸如Boolean
,Array
等等都会调用重写的方法,实际状况就是它们都有本身重写的toString。
三者均可用于重定义this对象,或者说重定义this指向。
上面看到了call:Object.prototype.toString.call(nun)
,call的做用是改变this的指向,这里nun是调用者,调用前面的Object.prototype.toString
方法,举个例子
function Person(name, age) { this.name = name; this.age = age; this.say = function () { console.log("myName is:" + this.name); } } var obj1 = {}; Person.call(obj1, "Tian", 21); //借用构造函数 console.log(obj1); //{name: "Tian", age: 21, say: ƒ} var obj2 = {}; Person.apply(obj2, ["Hui", 22]) console.log(obj2); //{name: "Hui", age: 22, say: ƒ} var fun = obj1.say; fun.bind(obj1)(); //fun.bind(obj1)返回的是一个函数
此处使用了Persn()
来构造obj,在开发中每每在某一构造函数彻底覆盖另外一构造函数时使用这种方法,以下,Student
的需求彻底覆盖了Person
,就能够在Student
中使用call
使用Person
的代码,而不用本身再写一遍,call
时企业级开发组装函数的一个方法之一
function Person(name, age, sex) { this.name = name; this.age = age; this.sex = sex; } function Student(name, age, sex, grade, tel) { Person.call(this, name, age, sex); this.grade = grade; this.tel = tel; } var nun = new Student("Tian", 21, "Male", 9, 188); console.log(nun); //Student {name: "Tian", age: 21, sex: "Male", grade: 9, tel: 188}
为何call()第一个参数传入this值得思考
apply的区别在于传参数不同,call是一个一个把参数传进去,而apply是一个arguments,也就是一个数组(不包括this),咱们只须要把call()除开this的其余参数用[]
括起来就能够了,以下
Person.apply(this, [name, age, sex]);
bind()
方法建立一个新的函数,在 bind()
被调用时,这个新函数的 this 被指定为 bind()
的第一个参数,而其他参数将做为新函数的参数,供调用时使用。
function Person(name, age) { this.name = name; this.age = age; this.say = function (ming) { console.log("myName is:" + ming); } } let obj1 = new Person(); var fun = obj1.say; fun.bind(obj1, "Hang")(); //myName is:Hang
console.log(0.14 * 100); //输出14.000000000000002,JS的精度不高 console.log(Math.ceil(123.33)); //输出124,向上取整 console.log(Math.floor(123.33)); //输出123,向下取整 console.log(Math.random()); //产生0~1之间的随机数 var nun = Number(123.654); console.log(nun.toFixed(2)); //123.65 // toFixed把Number四舍五入为指定小数位数的数字,此处指定2位 // 产生100之内随机数 for (var i = 0; i < 10; i++) { var num = Math.random(); console.log("随机数:" + num); num = num.toFixed(2); console.log("小数位:" + num); num = num * 100; console.log("乘一百:" + num); }
按道理来讲在for循环中最后输出的数字应该都是0~100的两位数,可是结果并不是如此,好比
let num = 0.5458565720681452; num = num.toFixed(2); num = num * 100; console.log(num); //55.00000000000001
一样这仍是由于精度不高,此处有一个解决办法就是先乘100再取整。
for (var i = 0; i < 10; i++) { var num = Math.random(); num = num * 100; num = num.toFixed(0); console.log(num); }
可计算范围:小数点前16位与后16位,若是超出请使用BigInt。
关于JavaScript的数据类型可参考 YAMA:JavaScript数据类型及变量
如下P xx表示《JavaScript高级程序设计》的第几页
ECMAScript变量可能包含两种不一样数据类型的值:基本类型值和引用类型值,前者指简单的数据段,后者指可能由多个值构成的对象
参考: YAMA:JavaScript数据类型
定义基本类型值和引用类型值的方式是相似的:建立一个变量并为其赋值。可是当值保存到变量中之后
把一个变量赋值给另外一个变量时:
如果基本数据类型,则会新建一个值,把该值复制到新变量的内存上,此时两个变量的值各自独立,互不干扰。
此时若是是引用类型值,则会复制指针的值,该指针指向存储在堆中的对象,赋值后两个变量将引用同一个对象,改变其中一个变量会影响另外一个变量 。
function Person() { this.name = "hui"; } var per1 = new Person(); var per2 = per1; console.log(per1.name); //输出hui per2.name = "tian"; console.log(per1.name); //输出tian
引用类型值复制示意图以下
JavaScript中,函数都是按照值来传递的,也就是说把函数外部的值赋值给函数内部的参数,就像把值从一个变量复制到另外一个变量同样(这里面分为基本类型值的传递和引用类型值的传递),对此我感到困惑,由于参数只能按值传递,而访问变量却有两种方式,那么在向函数传递引用类型值时到底时怎么传值的?
图片源自 浅析 JavaScript Clone:堆内存、栈内存
当变量复制引用类型值的时候,它是一个指针,指向存储在堆内存中的对象(堆内存中的对象没法直接访问,要经过这个对象在堆内存中的地址访问,再经过地址去查值(RHS查询,试图获取变量的源值),因此引用类型的值是按引用访问)
所谓的传值是由于这个在栈内存中的变量,也就是这个指针(个人意思是这个指针是原始值)是存储在栈上的一个指针,指向一个存储在堆内存中的对象,因此说JS函数传参必定是按值传递的,可是访问变量确实有两种方式。
传递基本数据类型的值好理解,其实传递引用类型值时传递的仍然是值,传递的时候会把这个值在内存中的地址复制给一个局部变量,也就是形参,所以形参的变化会反映在外部,以下代码就说明了这一点
function Person() { this.name = "hui"; } var per1 = new Person(); function setname(per) { per.name = "tong"; console.log("局部" + per.name); //输出 局部tong per = new Person(); console.log("局部" + per.name); //输出 局部hui per.name = "tian"; console.log("局部" + per.name); //输出 局部tian } console.log(per1.name); //输出hui setname(per1); console.log(per1.name); //输出tong // 最后一行输出为tong,说明在setname函数内部per = new Person();并无起做用, // 由于函数按值来传递
注意此处还在函数内部新建了一个同名的对象per,若是per1是按引用传递的,那么per就会自动修改成指向其name属性为tian的新对象。但当接下来在外部访问per1.name是dong,这说明在函数内部修改了参数的值但原始的引用仍然保持不变。实际上在函数内部重写per时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后当即被销毁。
这里重申:对于JS中的内存,究竟是怎么存的,文中只是给出了一种想得通的说法,真要追究到底,恐怕并不是如此。参考: https://juejin.im/post/684490...
关于做用域能够参考:JavaScript预编译、做用域
执行环境定义了变量或函数有权访问的其余数据,决定了它们各自的行为。 每一个执行环境都有一个 与之关联的变量对象,环境中定义的全部变量和函数都保存在这个对象中。虽然咱们 编写的代码没法访问这个对象,但解析器在处理数据时会在后台使用它。
根据宿主环境不一样,表示的执行环境也不同,在Chrome中全局执行环境就是window对象,在node中是Global 对象。
参考:
做用域是根据名称查找变量的一套规则。负责收集并维护有全部声明的标识符(变量)组成的一系列查询,并实施一套严格规则,肯定当前执行的代码对这些标识符的访问权限,以及保证对执行环境有权访问的全部变量和函数的有序访问。做用域链的前端,始终都是当前执行的代码所在环境的变量对象。若是这个环境是函数,则将其活动对象(AO)做为变量对象。
活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。做用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是做用域链中的最后一个对象。
还需注意在JS中if语句等等使用的大括号不算是块做用域。以下
if (true) { var color = "blue"; } alert(color); //"blue"
使用let或const会建立块做用域。
参考:MDN 内存管理
像C语言这样的底层语言通常都有底层的内存管理接口,好比 malloc()
和free()
。相反,JavaScript是在建立变量(对象,字符串等)时自动进行了分配内存,而且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。
在P180闭包就涉及到了垃圾回收,因为闭包致使匿名函数的做用域一直引用着这个活动对象,换句话说活动对象还留在内存中不能被销毁,这会致使内存泄漏。我猜测这里的垃圾回收方式就是引用技术垃圾收集。
无论什么程序语言,内存生命周期基本是一致的:
全部语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。
大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“哪些被分配的内存确实已经再也不须要了”。它每每要求开发人员来肯定在程序中哪一块内存再也不须要而且释放它。
高级语言解释器嵌入了“垃圾回收器”,它的主要工做是跟踪内存的分配和使用,以便当分配的内存再也不使用时,自动释放它。这只能是一个近似的过程,由于要知道是否仍然须要某块内存是没法断定的(没法经过某种算法解决)。
如上所述自动寻找是否一些内存“再也不须要”的问题是没法断定的。所以,垃圾回收实现只能有限制的解决通常问题。
参考: