模拟实现call,apply,bind,new关键字
/*
* call方法原理:
* call()方法使用一个指定的this值和单独给出的一个一个或多个参数来调用一个函数
* function.call(thisAgs,ar1,ar2,ar3....);
* */
function person(a,b,c){
console.log(this.name);
console.log(a,b,c);
}
var egg = {name:'liao jie'};
Function.prototype.newCall = function(obj,...arg){
var obj = Object(obj) || window;
obj.p = this; //这个this指的是调用newCall的那个函数,obj指的就是传入的对象
// console.log(obj);
// console.log(obj.p);
console.log(arg);
var result = obj.p(...arg);
delete obj.p;
return result;
}
var bibi = person.newCall(egg,'点赞','收藏');
console.log(bibi);
Function.prototype.newApply = function(obj,arg){
var obj = Object(obj) || window;
obj.p = this; //这个this指的是调用newCall的那个函数,obj指的就是传入的对象
// console.log(obj);
// console.log(obj.p);
console.log(arg);
var result = obj.p(...arg);
delete obj.p;
return result;
}
var bibi = person.newApply(egg,['liao jie','jack John',]);
console.log(bibi);
Function.prototype.newBind = function(obj,...arg){
var that = this;
return function (...arg1) {
arg1 = Array.prototype.slice.call(arg1);
that.apply(obj,arg.concat(arg1));
}
}
person.newBind(egg,'liao jie','jack John','廖先生')();
/*new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:
创建一个空的简单JavaScript对象(即{});
链接该对象(即设置该对象的构造函数)到另一个对象 ;
将步骤1新创建的对象作为this的上下文 ;
如果该函数没有返回对象,则返回this。
*/
function create(fn,...args){//fn是要new的函数,args是函数的参数
// 创建一个空对象obj,然后将fn的原型链接到obj的原型上
let obj = Object.create(fn.prototype);
// 绑定 this 实现继承,obj 可以访问到构造函数中的属性
let res = fn.apply(obj,args);
// 优先返回构造函数返回的对象,object/array/function优先返回,如果是其他类型则返回obj
return res instanceof Object ? res : obj;
}
理论
Object.prototype.toString.call 是如何判断变量的类型的
在toString方法被调用时,会执行下面的操作步骤:
对于 Object.prototype.toString.call(arg),若参数为 null 或 undefined,直接返回结果。
Object.prototype.toString.call(null); // => "[object Null]"
Object.prototype.toString.call(undefined); // => "[object Undefined]"
若参数不为 null 或 undefined,则将参数转为对象,再作判断。对于原始类型,转为对象的方法即装箱,此处不赘述。
转为对象后,取得该对象的 [Symbol.toStringTag] 属性值(可能会遍历原型链)作为 tag,如无该属性,或该属性值不为字符串类型,则依下表取得 tag, 然后返回 "[object " + tag + "]" 形式的字符串。
// Boolean 类型,tag 为 "Boolean"
Object.prototype.toString.call(true); // => "[object Boolean]"
// Number 类型,tag 为 "Number"
Object.prototype.toString.call(1); // => "[object Boolean]"
// String 类型,tag 为 "String"
Object.prototype.toString.call(""); // => "[object String]"
// Array 类型,tag 为 "String"
Object.prototype.toString.call([]); // => "[object Array]"
// Arguments 类型,tag 为 "Arguments"
Object.prototype.toString.call((function() {
return arguments;
})()); // => "[object Arguments]"
// Function 类型, tag 为 "Function"
Object.prototype.toString.call(function(){}); // => "[object Function]"
// Error 类型(包含子类型),tag 为 "Error"
Object.prototype.toString.call(new Error()); // => "[object Error]"
// RegExp 类型,tag 为 "RegExp"
Object.prototype.toString.call(/\d+/); // => "[object RegExp]"
// Date 类型,tag 为 "Date"
Object.prototype.toString.call(new Date()); // => "[object Date]"
// 其他类型,tag 为 "Object"
Object.prototype.toString.call(new class {}); // => "[object Object]"
垃圾回收机制
现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。
1、标记清除
这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。
从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。
而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。
最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
关于这一块,建议读读Tom大叔的几篇文章,关于作用域链的一些知识详解,读完差不多就知道了,哪些变量会被做标记。
2、引用计数
另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。
当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。
相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。
当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。
这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。
强引用和弱引用
强引用是指向内存申请一段存储空间,进行存储的引用类型的对象的引用,如下创建一个强引用,
object obj = new object();
obj = 10;
在对象获得的分配内存空间中不仅仅存放了对象的信息,还存放着该对象被引用的次数。
在创建一个强引用时,默认的引用次数为 1,之后每引用一次加 1,
object obj1 = obj;
此时,对象 obj 的引用次数为 2。
弱引用并不会增加对象的引用次数,下面声明一个对 obj 的弱引用,
WeakReference wr = new WeakReference(obj);
在对象 obj 被回收之前,弱引用始终可以访问 obj,即弱引用允许应用程序访问对象,同时也允许垃圾回收器回收相应的对象。
占用大量内存,但通过垃圾回收功能回收以后很容易重新创建的对象特别适合使用弱引用。
Map和WeakMap的区别
第一:和WeakSet一样,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
第二:WeakSet的键名所指的对象,不计入垃圾回收机制。
null和undefined的区别
null表示"没有对象",即该处不应该有值。典型用法是:
作为函数的参数,表示该函数的参数不是对象。
作为对象原型链的终点。
Object.getPrototypeOf(Object.prototype)// null
undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。典型用法是:
变量被声明了,但没有赋值时,就等于undefined。
调用函数时,应该提供的参数没有提供,该参数等于undefined。
对象没有赋值的属性,该属性的值为undefined。
函数没有返回值时,默认返回undefined。
var i;
i // undefined
function f(x){console.log(x)}
f() // undefined
var o = new Object();
o.p // undefined
var x = f();
x // undefined
内存泄露:
内存泄漏Memory Leak是指程序中已动态分配的堆内存由于疏忽或错误等原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
对于内存泄露的检测,Chrome提供了性能分析工具Performance,可以比较方便的查看内存的占用情况等。
1.意外的全局变量
JavaScript的目标是开发一种看起来像Java但足够自由的被初学者使用的语言。
JavaScript自由的其中一种方式是它可以处理没有声明的变量:一个未声明的变量的引用在全局对象中创建了一个新变量。
在浏览器的环境中,全局对象是window。也就是说:
function foo(arg) {
bar = "this is a hidden global variable";
}
实际上是:
1 function foo(arg) {
2 window.bar = "this is an explicit global variable";
3 }
如果bar是仅在foo函数作用域内承载引用,并且你忘记用var来声明的变量,一个意外的全局变量就被创建了。
在这个例子中,泄漏一个单一字符串不会有太大害处,但这的确是不好的。
另一种意外全局变量被创建的方式是通过this:
1 function foo() {
2 this.variable = "potential accidental global";
3 }
4 // Foo called on its own, this points to the global object (window)
5 // rather than being undefined.
6 foo();
为了阻止这种错误发生,在你的Javascript文件最前面添加’use strict;’。
这开启了解析JavaScript的阻止意外全局的更严格的模式。
全局变量的一个注意事项:
即使我们讨论了不明的全局变量,仍然存在很多代码被显式的全局变量填充的情况。
这是通过定义不可收集的情况(除非清零或重新赋值)。特别的,用来临时存储和处理大量信息的全局变量会引起关注。如果必须用全局变量来存储很多数据,在处理完之后,确保对其清零或重新赋值。 一个在与全局连接上增加内存消耗常见的原因是缓存)。 缓存存储重复被使用的数据。为此,为了有效,缓存必须有其大小的上限。飙出限制的缓存可能会因为内容不可被回收,导致高内存消耗。
2.被遗忘的计时器或回调
在JavaScript中setInterval的使用相当常见。其他库提供观察者和其他工具以回调。
这些库中大多数,在引用的实例变成不可访问之后,负责让回调的任何引用也不可访问。
在setInterval的情况下,这样的代码很常见:
1 var someResource = getData();
2 setInterval(function() {
3 var node = document.getElementById('Node');
4 if(node) {
5 // Do stuff with node and someResource.
6 node.innerHTML = JSON.stringify(someResource));
7 }
8 }, 1000);
这个例子表明了跳动的计时器可能发生什么:计时器使得节点或数据的引用不再被需要了。
代表node的对象将来可能被移除,使得整个块在间隔中的处理不必要。
然而,由于间隔仍然是活跃的,处理函数不能被回收(间隔需要被停掉才能回收)。
如果间隔处理不能被回收,它的依赖也不能被回收。那意味着可能存储着大量数据的someResource也不能被回收。
观察者情况下,一旦不被需要(或相关的对象快要访问不到)就创建明确移除他们的函数很重要。
在过去,这由于特定浏览器(IE6)不能很好的管理循环引用(下面有更多相关信息),曾经尤为重要。
现如今,一旦观察对象变成不可访问的,即使收听者没有明确的被移除,多数浏览器会回收观察者处理函数。
然而,在对象被处理前明确的移除这些观察者是一种好的实现。例如:
1 var element = document.getElementById('button');
2 function onClick(event) {
3 element.innerHtml = 'text';
4 }
5 element.addEventListener('click', onClick);
6 // Do stuff
7 element.removeEventListener('click', onClick);
8 element.parentNode.removeChild(element);
9 // Now when element goes out of scope,
10 // both element and onClick will be collected even in old browsers that don't
11 // handle cycles well.
一条关于对象观察者及循环引用的笔记
观察者和循环引用曾经是JavaScript开发者的祸患。这是由于IE垃圾回收的一个bug(或者设计决议)出现的情况。
IE的老版本不能检测到DOM节点和JavaScript代码间的循环引用。
这是一个通常为观察到的保留引用(如同上面的例子)的观察者的典型。
也就是说,每次在IE中对一个节点添加观察者的时候,会导致泄漏。
这就是为什么开发者会在节点删除前会明确的移除处理函数或给引用赋空值。
现在,现代浏览器(包括IE和MS Edge)使用现代垃圾回收算法,可以检测到这些循环并正确处理它们。
换言之,在删除节点前,调用removeEventLister并不是严格意义上必须的。
- 超出DOM引用
把DOM节点存储到数据结构中。
假设你想要迅速的更新一个表格中的几行内容。
可以把每个DOM行的引用存储到一个字典或数组中。
同个DOM元素有两个引用:一个在DOM树中,另外一个在字典中。
如果之后你决定要移除这些行,需要让两个引用都无效。
1 var elements = {
2 button: document.getElementById('button'),
3 image: document.getElementById('image'),
4 text: document.getElementById('text')
5 };
6 function doStuff() {
7 image.src = 'http://some.url/image';
8 button.click();
9 console.log(text.innerHTML);
10 // Much more logic
11 }
12 function removeButton() {
13 // The button is a direct child of body.
14 document.body.removeChild(document.getElementById('button'));
15 // At this point, we still have a reference to #button in the global
16 // elements dictionary. In other words, the button element is still in
17 // memory and cannot be collected by the GC.
18 }
对此,必须处理DOM树内的内部节点或叶子节点。
假设你在JavaScript代码中保留了一个对于特定的表格内节点(一个标签)的引用。
之后决定从DOM中移除这个表格,但是保留对于那个节点的引用。你可能假设GC(garbage collector)会回收除那个节点之外的每个节点。
但是在实际中:这个单节点是那个表格的子节点,子节点保留对父节点引用。
换句话说,来自JavaScript代码的表格元素的引用会引起在内存里存整个表格。
4.闭包
一个JavaScript开发的关键点是闭包:匿名函数内的变量从父级作用域捕获。
根据JavaScript runtime的实现细节,很多开发者发现有以一种微妙的方式存在泄漏的可能,这种特殊的情况:
1 var theThing = null;
2 var replaceThing = function () {
3 var originalThing = theThing;
4 var unused = function () {
5 if (originalThing)
6 console.log("hi");
7 };
8 theThing = {
9 longStr: new Array(1000000).join('*'),
10 someMethod: function () {
11 console.log(someMessage);
12 }
13 };
14 };
15 setInterval(replaceThing, 1000);
这个代码片段做了一件事:每次replaceThing被调用的时候,theThing获取到一个包括一个大数组和新闭包(somMethod)的新对象。
同时,变量unused保留了一个有originalThing(theThing之前在replaceThing被调用)引用的闭包。
已经有点疑惑了,哈?重要的是一旦一个作用域被创建于同个父作用域下的多个闭包,那个作用域是共享的。
这种情况下,为闭包somMethod创建的作用域被unused共享了。
unused有一个对originalThing的引用。
即使unused从来没被用过,someMethod可以通过theTing被使用。
由于someMethod和unused共享了闭包作用域,即使unused从来没被用过,它对originalThing的引用迫使它停留在活跃状态(不能回收)。
当这个代码片段重复运行的时候,可以看到内存使用稳步的增长。
GC运行的时候,这并不会减轻。
本质上,一组关联的闭包被创建(同unused变量在表单中的根节点一起),这些闭包作用域中每个带了大数组一个非直接的引用,导致了大型的泄漏。
避免:
1.垃圾自动回收
2.使用浏览器工具(具体的不会)