jkfhto / js_study Goto Github PK
View Code? Open in Web Editor NEWjs 学习 总结
js 学习 总结
窗口的resize、scroll、输入框内容校验等操作时,如果这些操作处理函数是较为复杂或页面频繁重渲染等操作时,在这种情况下如果事件触发的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时我们可以采用debounce(防抖)
和throttle(节流)
的方式来减少触发的频率
,同时又不影响实际效果。
当持续触发事件时,debounce 会合并事件且不会去触发事件,当一定时间内没有触发再这个事件时,才真正去触发事件,只会触发最后一次。
function debounce(callback, delay) {
var timer = null, context, args;
return function (e) {
// 绑定this
context = this;
// 获取参数
args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
callback.apply(context, args)
}, delay);
}
}
window.onscroll = debounce(function (e) {
console.log("触发滚动", e)
}, 500)
不希望非要等到事件停止触发后才执行,希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
加个 immediate 参数判断是否是立刻执行。
function debounce(callback, delay, immediate) {
var timer = null, context, args;
return function (e) {
// 绑定this
context = this;
// 获取参数
args = arguments;
clearTimeout(timer);
if (immediate) {
// 判断绑定的函数是否已经执行了 如果已经执行过,不再执行
var didRun = !timer;
if (didRun) {
//没有执行过,立刻执行
callback.apply(context, args);
}
}
timer = setTimeout(() => {
callback.apply(context, args)
}, delay);
}
}
此时注意一点,就是绑定的函数可能是有返回值的,所以我们也要返回函数的执行结果,但是当 immediate
为 false 的时候,因为使用了 setTimeout
,我们将 func.apply(context, args)
的返回值赋给变量,最后再return
的时候,值将会一直是undefined
,所以我们只在 immediate
为 true
的时候返回函数的执行结果。
function debounce(callback, delay, immediate) {
var timer = null, context, args;
var result;
return function (e) {
// 绑定this
context = this;
// 获取参数
args = arguments;
clearTimeout(timer);
if (immediate) {
// 判断绑定的函数是否已经执行了 如果已经执行过,不再执行
var didRun = !timer;
if (didRun) {
//没有执行过,立刻执行
result = callback.apply(context, args);
}
}
timer = setTimeout(() => {
callback.apply(context, args)
}, delay);
// 返回值
return result;
}
}
最后我们再思考一个小需求,我希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行啦。
function debounce(callback, delay, immediate) {
var timer = null, context, args;
var result;
var debounced = function (e) {
// 绑定this
context = this;
// 获取参数
args = arguments;
clearTimeout(timer);
if (immediate) {
// 判断绑定的函数是否已经执行了 如果已经执行过,不再执行
var didRun = !timer;
if (didRun) {
//没有执行过,立刻执行
result = callback.apply(context, args);
}
}
timer = setTimeout(() => {
callback.apply(context, args)
}, delay);
// 返回值
return result;
}
// 取消防抖
debounced.cancel = function () {
// 清除timeout
clearTimeout(timeout);
// 重置timeout
timeout = null;
};
return debounced;
}
规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。
我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。
关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
function throttle(callback, delay) {
var previous = 0;
var context, args;
return function () {
// 绑定this
context = this;
// 获取参数
args = arguments;
var now = new Date().getTime();
if (now - previous > delay) {
previous = now;
callback.apply(context, args);
}
}
}
接下来,我们讲讲第二种实现方式,使用定时器。
当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,重置定时器,这样就可以设置下个定时器。
function throttle(callback, delay) {
var timeout;
var previous = 0;
return function () {
// 绑定this
context = this;
// 获取参数
args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
// 重置定时器
timeout = null;
callback.apply(context, args)
}, delay)
}
}
}
比较使用时间戳,使用定时器两个方法:
第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件
想要控制第一次是否立刻执行,事件停止触发后是否会再执行一次事件。
那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:
leading:false 表示禁用第一次执行
trailing: false 表示禁用停止触发的回调
默认会开启第一次立刻执行,事件停止触发后会再执行一次事件
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function () {
// 如果禁用第一次立刻执行 设置previous为0,throttled函数remaining会确保大于0 throttled函数会执行else if条件语句里面的内容,从而达到节流的效果,如果没有禁用第一次立刻执行,需要重置previous为当前时间
previous = options.leading === false ? 0 : new Date().getTime();
// 执行完重置timeout
timeout = null;
// 执行函数
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function () {
var now = new Date().getTime();
// 触发第一次操作时,默认会第一次立刻执行,第一次执行remaining会小于0,程序会执行 if条件语句的内容,快速执行第二次点击previous = now;remaining会大于0,会执行else if条件语句里面的内容
// 触发第一次操作时,如果禁用第一次立刻执行,当前previous为0,将previous设置为now,remaining会大于0 程序会执行else if条件语句里面的内容
if (!previous && options.leading === false) previous = now;
// 获取触发函数剩余的时间
var remaining = wait - (now - previous);
// 绑定this
context = this;
// 绑定arguments
args = arguments;
if (remaining <= 0 || remaining > wait) {
// 清除timeout
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
// 重置时间
previous = now;
// 执行函数
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 事件停止触发后会再执行一次事件
// 通过计时器setTimeout,设置remaining时间后执行事件触发,达到节流
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
https://juejin.im/post/5b8de829f265da43623c4261
https://juejin.im/post/5b7b88d46fb9a019e9767405?utm_medium=fe&utm_source=weixinqun
因为 new 是关键字,所以无法像 bind 函数一样直接覆盖,所以我们写一个函数,命名为 mockNew,来模拟 new 的效果。
function mockNew() {
// 需要返回的对象
var obj = new Object();
// 获取需要实例化的构造函数
// shift 会返回构造函数,修改原数组,所以 arguments 会被去除第一个参数
var Constructor = [].shift.call(arguments);
// 继承原型上的属性和方法
obj.__proto__ = Constructor.prototype;
//继承构造函数的属性和方法
Constructor.apply(obj, arguments);
return obj;
}
我们还需要判断构造函数的返回值,如果构造函数的返回值是一个引用类型的对象,我们就返回构造函数的返回值,如果不是,我们该返回什么就返回什么。
function mockNew() {
// 需要返回的对象
var obj = new Object();
// 获取需要实例化的构造函数
// shift 会返回构造函数,修改原数组,所以 arguments 会被去除第一个参数
var Constructor = [].shift.call(arguments);
// 继承原型上的属性和方法
obj.__proto__ = Constructor.prototype;
// 继承构造函数的属性和方法
var result = Constructor.apply(obj, arguments);
// 兼容返回值类型
// 如果构造函数的返回值是一个引用类型的对象,我们就返回构造函数的返回值,否则返回obj
return result instanceof Object ? result : obj;
}
function Parent () {
this.name = 'kevin';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName()) // kevin
核心: 将父类的实例作为子类的原型
特点:实例可继承的属性有:Child实例的构造函数的属性,父类构造函数属性,父类原型的属性。
缺点:
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
Parent.call(this);
}
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy"]
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类。
特点:
缺点:
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child1 = new Child('kevin', '18');
child1.colors.push('black');
console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]
var child2 = new Child('daisy', '20');
console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]
核心:原型链继承和经典继承双剑合璧。
优点:可以继承父类原型上的属性,可以传参,可复用。融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
缺点:调用了两次父类构造函数(耗内存)。
function createObj(o) {
// 创建一个空函数
function F(){}
// o一般指向父类原型
F.prototype = o;
// 实例化空函数,可以继承父类原型上的方法和属性
return new F();
}
就是 ES5 Object.create
的模拟实现,将传入的对象作为创建的对象的原型。
var person = {
name: 'kevin',
friends: ['daisy', 'kelly']
}
var person1 = createObj(person);
var person2 = createObj(person);
person1.name = 'person1';
console.log(person2.name); // kevin
person1.firends.push('taylor');
console.log(person2.friends); // ["daisy", "kelly", "taylor"]
注意:修改person1.name
的值,person2.name
的值并未发生改变,并不是因为person1
和person2
有独立的 name
值,而是因为person1.name = 'person1'
,给person1
添加了 name
值,并非修改了原型上的 name
值,person2.name
获取的是原型上的值。
核心:使用一个空函数作为过渡对象,让空函数的prototype 指向需要继承的对象,返回实例化的空函数。
优点:空函数作为过渡对象,构造函数无内容,可以较少开销。
缺点:包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
function createObj (o) {
var clone = createObj(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}
核心:寄生式继承就是对原型式继承的第二次封装,在第二次封装过程中对继承的对象进行了扩展,这样新创建的对象不仅可以继承父类中的属性和方法而且还添加了新的属性和方法。
// 原型式继承
function createObj(o) {
// 创建一个空函数
function F(){}
// o一般指向父类原型
F.prototype = o;
// 实例化空函数,可以继承父类原型上的方法和属性
return new F();
}
// 寄生式继承
function prototype(child, parent) {
// 通过原型式继承,继承父类原型上的属性和方法
var prototype = createObj(parent.prototype);
// 添加新的属性,修复子类的构造函数
prototype.constructor = child;
// 修改子类的原型,保留子类的构造函数,继承父类原型上的属性和方法
child.prototype = prototype;
}
// 当我们使用的时候:
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
// 借用构造函数
Parent.call(this, name);
this.age = age;
}
prototype(Child, Parent);
var child1 = new Child('kevin', '18');
console.log(child1);
核心:寄生式继承和借用构造函数的经典组合。
借用构造函数,继承了构造函数中的属性和方法。通过原型继承可以继承父类原型的属性和方法,由于原型继承中,使用空函数作为过渡对象,构造函数无内容,可以较少开销。直接通过原型继承存在一个问题,子类的构造函数将会丢失。所以通过,寄生式继承对原型继承的结果进行扩展,修复其构造函数指向的不正确问题。最后将子类原型指向这个结果。
寄生组合式继承是引用类型最理想的继承范式。
Function.prototype.call = function (context) {
var context = context ? Object(context) : window;// 兼容context,Object(context)兼容context为string的情况
context.fn = this; // 将函数设为对象的属性
var args = [];
//获取参数
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
// 执行函数
// args 会自动调用 Array.toString() 这个方法
var result = eval('context.fn(' + args +')');
// 删除该函数
delete context.fn;
//返回结果
return result;
}
Function.prototype.apply = function (context, args) {
var context = context ? Object(context) : window;// 兼容context,Object(context)兼容context为string的情况
context.fn = this; // 将函数设为对象的属性
var result = null;
// 执行函数
if (!args) {
// 没有传参直接直接函数
result = context.fn()
} else {
arr = [];
for (var i = 0, len = args.length; i < len; i++) {
arr.push('args[' + i + ']');
}
// arr 会自动调用 Array.toString() 这个方法
result = eval('context.fn(' + arr + ')');
}
// 删除该函数
delete context.fn;
//返回结果
return result;
}
eval()函数可以让一段代码字符串动态执行
eval()函数的参数是一个字符串,如果字符串表示表达式,则eval()计算表达式。如果参数表示一个或多个JavaScript语句,则eval()会对语句进行执行。
1.降低性能。
2.安全问题。因为它的动态执行特性,给被求值的字符串赋予了太大的权力,于是大家担心可能因此导致 XSS 等攻击。
3.调试困难。eval 就像一个黑盒,其执行的代码很难进行断点调试。
把接收多个参数的函数变换成接收一个单一参数(最初函数的第一个参数)的函数,并返回接受剩余的参数而且返回结果的新函数的技术。
简单的说,柯里化函数持续地返回一个新函数直到所有的参数用尽为止。这些参数全部保持“活着”的状态(通过闭包),然后当柯里化链中的最后一个函数被返回和执行时会全部被用来执行。
提高函数的适用性,同时降低函数的通用性;其实现方式就是固定一些可以预期的参数,然后返回一个特定的函数
// 普通的add函数
function add(x, y) {
return x + y
}
// Currying后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3
// 正常正则验证字符串 reg.test(txt)
// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g, 'test') //false
check(/[a-z]+/g, 'test') //true
// Currying后
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // false
//兼容现代浏览器以及IE浏览器的事件添加方法
var addEvent = function(element, event, handler) {
if (document.addEventListener) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
} else {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
}
}
上面的方法有什么问题呢?很显然,我们每次使用addEvent为元素添加事件的时候,会走一遍if...else if ...,其实只要一次判定就可以了,怎么做?–柯里化。它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了浏览器支持的事件绑定,避免每次都进行判断。改为下面这样子的代码:
var addEvent = (function() {
if (document.addEventListener) {
return function(element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
};
} else {
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
};
}
})();
Function.prototype.bind = function (target) {
const self = this;
return function () {
self.apply(target, arguments)
}
}
https://segmentfault.com/a/1190000006096034
https://www.jianshu.com/p/2975c25e4d71
用var声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,也可以是声明在任何函数外的变量。let和const是块级作用域,意味着它们只能在最近的一组花括号(function、if-else 代码块或 for 循环中)中访问。
function foo() {
// 所有变量在函数中都可访问
var bar = 'bar';
let baz = 'baz';
const qux = 'qux';
console.log(bar); // bar
console.log(baz); // baz
console.log(qux); // qux
}
console.log(bar); // ReferenceError: bar is not defined
console.log(baz); // ReferenceError: baz is not defined
console.log(qux); // ReferenceError: qux is not defined
if (true) {
var bar = 'bar';
let baz = 'baz';
const qux = 'qux';
}
// 用 var 声明的变量在函数作用域上都可访问
console.log(bar); // bar
// let 和 const 定义的变量在它们被定义的语句块之外不可访问
console.log(baz); // ReferenceError: baz is not defined
console.log(qux); // ReferenceError: qux is not defined
var会使变量提升,这意味着变量可以在声明之前使用。let和const不会使变量提升,提前使用会报错。
console.log(foo); // undefined
var foo = 'foo';
console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initialization
let baz = 'baz';
console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initialization
const bar = 'bar';
用var重复声明不会报错,但let和const会。
var foo = 'foo';
var foo = 'bar';
console.log(foo); // "bar"
let baz = 'baz';
let baz = 'qux'; // Uncaught SyntaxError: Identifier 'baz' has already been declared
let和const的区别在于:let允许多次赋值,而const只允许一次。
// 这样不会报错。
let foo = 'foo';
foo = 'bar';
// 这样会报错。
const baz = 'baz';
baz = 'qux';
全局作用域下,使用var声明的变量会挂载到window对象上,使用let,const声明的变量不会挂载到window对象上
let a = 10;
const b = 20;
var c =30;
window.a; // undefined
window.b; // undefined
window.c; // 30
let a = () =>{};
console.log(a.prototype); // undefined
a.__proto__===Function.prototype;// true
2.箭头函数的this指向它被创建时的上下文
const obj = {
id: 22,
print: () => {
console.log(this.id);
},
print2: function(){
console.log(this.id);
},
print3: function () {
setTimeout(function(){
console.log(this.id);
}, 500);
},
print4: function () {
setTimeout(() => {
console.log(this.id);
}, 500);
}
}
obj.print(); // undefined
obj.print2(); // 22
obj.print3(); // undefined
obj.print4(); // 22
3.在构造函数里使用箭头函数的主要优点是它的 this 只与箭头函数创建时的 this 保持一致,并且不会修改。所以,当用构造函数去创建一个新的对象的时候,箭头函数的 this 总是指向新创建的对象。正常函数的 this 是可以在执行过程中被改变的,而箭头函数的 this 则会一直保持一致。所以在使用箭头函数的时候,你就不需要担心它的上下文被改变了。
const Person = function (firstName) {
this.firstName = firstName;
this.sayName1 = function () { console.log(this.firstName); };
this.sayName2 = () => { console.log(this.firstName); };
};
const john = new Person('John');
const dave = new Person('Dave');
john.sayName1(); // John
john.sayName2(); // John
// 普通函数的 this 可以被修改,而箭头函数则不会
john.sayName1.call(dave); // Dave (因为 "this" 现在指向了 dave 对象)
john.sayName2.call(dave); // John
john.sayName1.apply(dave); // Dave (因为 "this" 现在指向了 dave 对象)
john.sayName2.apply(dave); // John
john.sayName1.bind(dave)(); // Dave (因为 "this" 现在指向了 dave 对象)
john.sayName2.bind(dave)(); // John
var sayNameFromWindow1 = john.sayName1;
sayNameFromWindow1(); // undefined (因为 "this" 现在指向了 Window 对象)
var sayNameFromWindow2 = john.sayName2;
sayNameFromWindow2(); // John
4.如果箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向window(全局对象)
'use strict'
const obj = {
print: () => {
console.log(this);
}
}
obj.print(); // Window
5.更简洁的语法
箭头函数
var a = ()=>{
return 1;
}
相当于普通函数
function a(){
return 1;
}
6.不能使用new
箭头函数作为匿名函数,是不能作为构造函数的,不能使用new
var B = ()=>{
value:1;
}
var b = new B(); //TypeError: B is not a constructor
7.不绑定arguments,用rest参数...解决
/*常规函数使用arguments*/
function test1(a){
console.log(arguments); //1
}
/*箭头函数不能使用arguments*/
const test2 = (a)=>{console.log(arguments)} //ReferenceError: arguments is not defined
/*箭头函数使用reset参数...解决*/
let test3=(...a)=>{console.log(a)} //22
test1(1);
test2(2);
test3(11,22,33);
1.不能简单返回对象字面量
如果要返回对象时需要用小括号包起来,因为大括号被占用解释为代码块了,正确写法
var func1 = () => { foo: 1 }; // undefined 想返回一个对象,大括号被占用解释为代码块,执行后返回undefined
var func2 = () => ({ foo: 1 }); // { foo: 1 }
2.箭头函数不能当做Generator函数,不能使用yield关键字
3.箭头函数不能换行
let a = ()
=>1; //SyntaxError: Unexpected token =>
粗略地讲,函数的调用方式决定了this的值,谁调用的函数,this就指向谁。this取值符合以下规则:
1.如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象。
2.如果apply、call或bind方法用于调用、创建一个函数,this的值就取传入的对象的值。
3.如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的this指向该对象。比如当obj.method()被调用时,函数内的 this 将绑定到obj对象。
4.如果调用函数不符合上述规则,那么this的值指向全局对象(global object)。浏览器环境下this的值指向window对象,但是在严格模式下('use strict'),this的值为undefined。
5.如果符合上述多个规则,则较高的规则(1 号最高,4 号最低)将决定this的值。
6.如果该函数是 ES2015 中的箭头函数,将忽略上面的所有规则,this被设置为它被创建时的上下文。
Javascript 变量的生命周期要分开来看,对于全局变量,他的生命周期会持续到页面关闭。而对于局部变量,在所在的函数的代码执行之后,局部变量的生命周期结束,他所占用的内存会通过垃圾回收机制释放(即垃圾回收).全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量,以确保性能问题。
JS环境中分配的内存一般有如下生命周期:
程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
有些语言(比如 C 语言)必须手动释放内存,程序员负责内存管理。
这很麻烦,所以大多数语言提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制"(garbage collector)。
垃圾回收机制怎么知道,哪些内存不再需要呢?
垃圾回收有两种方法:标记清除
、引用计数
。引用计数不太常用,标记清除较为常用。
这是javascript中最常用的垃圾回收方式。标记清除算法将“不再使用的对象”定义为“无法达到的对象”。简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。但反之未必成立。
算法由以下几步组成:
1、垃圾回收器创建了一个“roots”列表。roots 通常是代码中全局变量的引用。JavaScript 中,“window” 对象是一个全局变量,被当作 root 。window 对象总是存在,因此垃圾回收器可以检查它和它的所有子对象是否存在(即不是垃圾);
2、所有的 roots 被检查和标记为激活(即不是垃圾)。所有的子对象也被递归地检查。从 root 开始的所有对象如果是可达的,它就不被当作垃圾。
3、所有未被标记的内存会被当做垃圾,收集器现在可以释放内存,归还给操作系统了。
现代的垃圾回收器改良了算法,但是本质是相同的:可达内存被标记,其余的被当作垃圾回收。
另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。
但是引用计数有个最大的问题: 循环引用
function func() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。
要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:
obj1 = null;
obj2 = null;
function foo(arg) {
bar = "this is a hidden global variable";
}
bar没被声明,会变成一个全局变量,在页面关闭之前不会被释放。
另一种意外的全局变量可能由 this 创建:
function foo() {
this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();
在 JavaScript 文件头部加上 'use strict',可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。
var leaks = (function(){
var leak = 'xxxxxx';// 函数内部遍历被外部引用,导致执行上下文环境无法销毁,产生闭包
return function(){
console.log(leak);
}
})()
//解除引用 释放内存
leaks = null;
有时候出于优化性能的目的,我们会用一个变量暂存 节点,接下来使用的时候就不用再从 DOM 中去获取.但是在移除 DOM 节点的时候却忘记了解除暂存的变量对 DOM 节点的引用,也会造成内存泄漏
var element = {
image: document.getElementById('image'),
button: document.getElementById('button')
};
document.body.removeChild(document.getElementById('image'));
// 如果element没有被回收,这里移除了 image 节点也是没用的,image 节点依然留存在内存中.
与此类似情景还有: DOM 节点绑定了事件, 但是在移除的时候没有解除事件绑定,那么仅仅移除 DOM 节点也是没用的
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
上面的例子表明,在节点node或者数据不再需要时,定时器依旧指向这些数据。所以哪怕当node节点被移除后,interval 仍旧存活并且垃圾回收器没办法回收,它的依赖也没办法被回收,除非终止定时器。
var element = document.getElementById('button');
function onClick(event) {
element.innerHTML = 'text';
}
element.addEventListener('click', onClick);
对于上面的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。因为老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。
但是,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经可以正确检测和处理循环引用了。即回收节点内存时,不必非要调用 removeEventListener 了。将element赋值为null即可。
新版本的chrome在 performance 中查看:
步骤:
图中 Heap 对应的部分就可以看到内存在周期性的回落也可以看到垃圾回收的周期,如果垃圾回收之后的最低值(我们称为min),min在不断上涨,那么肯定是有较为严重的内存泄漏问题。
将[]赋值给一个数组对象,是清空数组的捷径(例如: arr = [];),但是需要注意的是,这种方式又创建了一个新的空对象,并且将原来的数组对象变成了一小片内存垃圾!实际上,将数组长度赋值为0(arr.length = 0)也能达到清空数组的目的,并且同时能实现数组重用,减少内存垃圾的产生。
为了最大限度的实现对象的重用,应该像避使用new语句一样避免使用{}来新建对象。
{“foo”:”bar”}这种方式新建的带属性的对象,常常作为方法的返回值来使用,可是这将会导致过多的内存创建,因此最好的解决办法是:每一次函数调用完成之后,将需要返回的数据放入一个全局的对象中,并返回此全局对象。如果使用这种方式,就意味着每一次方法调用都会导致全局对象内容的修改,这有可能会导致错误的发生。因此,一定要对此全局对象的使用进行详细的注释和说明。
有一种方式能够保证对象(确保对象prototype上没有属性)的重复利用,那就是遍历此对象的所有属性,并逐个删除,最终将对象清理为一个空对象。
cr.wipe(obj)方法就是为此功能而生,代码如下:
// 删除obj对象的所有属性,高效的将obj转化为一个崭新的对象!
cr.wipe = function (obj) {
for (var p in obj) {
if (obj.hasOwnProperty(p))
delete obj[p];
}
};
有些时候,你可以使用cr.wipe(obj)方法清理对象,再为obj添加新的属性,就可以达到重复利用对象的目的。虽然通过清空一个对象来获取“新对象”的做法,比简单的通过{}来创建对象要耗时一些,但是在实时性要求很高的代码中,这一点短暂的时间消耗,将会有效的减少垃圾堆积,并且最终避免垃圾回收暂停,这是非常值得的!
方法一般都是在初始化的时候创建,并且此后很少在运行时进行动态内存分配,这就使得导致内存垃圾产生的方法,找起来就不是那么容易了。但是从另一角度来说,这更便于我们寻找了,因为只要是动态创建方法的地方,就有可能产生内存垃圾。例如:将方法作为返回值,就是一个动态创建方法的实例。
在游戏的主循环中,setTimeout或requestAnimationFrame来调用一个成员方法是很常见的,例如:
setTimeout(
(function(self) {
return function () {
self.tick();
};
})(this), 16)
每过16毫秒调用一次this.tick(),嗯,乍一看似乎没什么问题,但是仔细一琢磨,每一次调用都返回了一个新的方法对象,这就导致了大量的方法对象垃圾!
为了解决这个问题,可以将作为返回值的方法保存起来,例如:
// at startup
this.tickFunc = (
function(self) {
return function() {
self.tick();
};
}
)(this);
// in the tick() function
setTimeout(this.tickFunc, 16);
相比于每次都新建一个方法对象,这种方式在每一帧当中重用了相同的方法对象。这种方式的优势是显而易见的,而这种**也可以应用在任何以方法为返回值或者在运行时创建方法的情况当中。
https://juejin.im/post/5cb33660e51d456e811d2687
https://segmentfault.com/a/1190000015641168
https://www.jianshu.com/p/a8a04fd00c3c
https://www.cnblogs.com/hustskyking/archive/2013/04/27/garbage-collection.html
https://juejin.im/post/5b10ba336fb9a01e66164346
当你没有提前使用var
、let
或const
声明变量,就为一个变量赋值时,该变量是未声明变量(undeclared variables)。未声明变量会脱离当前作用域,成为全局作用域下定义的变量。在严格模式下,给未声明的变量赋值,会抛出ReferenceError错误。和使用全局变量一样,使用未声明变量也是非常不好的做法,应当尽可能避免。要检查判断它们,需要将用到它们的代码放在try/catch语句中。
function foo() {
x = 1; // 在严格模式下,抛出 ReferenceError 错误
}
foo();
console.log(x); // 1
当一个变量已经声明,但没有赋值时,该变量的值是undefined
。如果一个函数的执行结果被赋值给一个变量,但是这个函数却没有返回任何值,那么该变量的值是undefined
。要检查它,需要使用严格相等(===);或者使用typeof
,它会返回'undefined'字符串。请注意,不能使用非严格相等(==)来检查,因为如果变量值为null,使用非严格相等也会返回true。
var foo;
console.log(foo); // undefined
console.log(foo === undefined); // true
console.log(typeof foo === 'undefined'); // true
console.log(foo == null); // true. 错误,不要使用非严格相等!
function bar() {}
var baz = bar();
console.log(baz); // undefined
null
只能被显式赋值给变量。它表示空值,与被显式赋值 undefined 的意义不同。要检查判断null值,需要使用严格相等运算符。请注意,和前面一样,不能使用非严格相等(==)来检查,因为如果变量值为undefined,使用非严格相等也会返回true。
var foo = null;
console.log(foo === null); // true
console.log(foo == undefined); // true. 错误,不要使用非严格相等!
作为一种个人习惯,我从不使用未声明变量。如果定义了暂时没有用到的变量,我会在声明后明确地给它们赋值为null。
null 表示一个值被定义了,定义为“空值”;
undefined 表示根本不存在定义。
所以设置一个值为 null 是合理的,如
objA.valueA = null;
但设置一个值为 undefined 是不合理的
null表示"没有对象",即该处不应该有值。典型用法是:
Object.getPrototypeOf(Object.prototype)// null
undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。典型用法是:
http://www.ruanyifeng.com/blog/2014/03/undefined-vs-null.html
Function.prototype.bind = function (context) {
var that = this;
return function () {
return self.apply(context);
}
}
bind 的时候可以传递参数,进行绑定
Function.prototype.bind = function (context) {
var that = this;
//绑定参数
// 获取bind函数从第二个参数到最后一个参数
var bindArgs = Array.prototype.slice.call(arguments, 1)
return function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var args = Array.prototype.slice.call(arguments)
// 指向函数 返回结果
// 最终执行结果的参数,需要将bindArgs,args使用concat合并
return that.apply(context, bindArgs.concat(args))
}
}
bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效
Function.prototype.bind = function (context) {
var that = this;
// 获取bind函数从第二个参数到最后一个参数
var bindArgs = Array.prototype.slice.call(arguments, 1)
var fBound = function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var args = Array.prototype.slice.call(arguments)
// 指向函数 返回结果
// 最终执行结果的参数,需要将bindArgs,args使用concat合并
// bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,this指向fBound,但传入的参数依然生效
return that.apply(this instanceof fBound ? this : context, bindArgs.concat(args))
}
return fBound;
}
Function.prototype.bind = function (context) {
var that = this;
// 获取bind函数从第二个参数到最后一个参数
var bindArgs = Array.prototype.slice.call(arguments, 1)
var fNOP = function () { };
var fBound = function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var args = Array.prototype.slice.call(arguments)
// 指向函数 返回结果
// 最终执行结果的参数,需要将bindArgs,args使用concat合并
// bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,this指向fBound,但传入的参数依然生效
return that.apply(this instanceof fBound ? this : context, bindArgs.concat(args))
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
// 直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype
// fBound.prototype = this.prototype;
// 通过一个空函数来进行中转
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
Function.prototype.bind = function (context) {
// 调用bind的不是函数,直接报错
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var that = this;
// 获取bind函数从第二个参数到最后一个参数
var bindArgs = Array.prototype.slice.call(arguments, 1)
var fNOP = function () { };
var fBound = function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var args = Array.prototype.slice.call(arguments)
// 指向函数 返回结果
// 最终执行结果的参数,需要将bindArgs,args使用concat合并
// bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,this指向fBound,但传入的参数依然生效
return that.apply(this instanceof fBound ? this : context, bindArgs.concat(args))
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
// 直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype
// fBound.prototype = this.prototype;
// 通过一个空函数来进行中转
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
==
是抽象相等运算符,而===
是严格相等运算符。
==
运算符是在进行必要的类型转换后,再比较。
===
运算符不会进行类型转换,所以如果两个值不是相同的类型,会直接返回false。
使用==
时,可能发生一些特别的事情,例如:
1 == '1'; // true
1 == [1]; // true
1 == true; // true
0 == ''; // true
0 == '0'; // true
0 == false; // true
除了方便与null或undefined比较时,最好不使用==运算符
.call和.apply都用于调用函数,第一个参数将用作函数内 this 的值。然而,.call接受逗号分隔的参数作为后面的参数,而.apply接受一个参数数组作为后面的参数。
function add(a, b) {
return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3
bind()方法返回一个新的函数, 新函数的this值指向传入的第一个参数,this被绑定了
面向对象编程与面向过程编程是一种编程**,与具体语言无关。
面向过程编程
面向过程编程是一种以过程为中心的编程**,分析出解决问题的步骤,然后用函数把这些步骤一步一步实现。面向过程编程,数据和对数据的操作是分离的。
按照步骤 1=》2=》3
面向对象编程
面向对象编程是将事物对象化,通过对象通信来解决问题。面向对象编程,数据和对数据的操作是绑定在一起的。
面向对象的三大基本特征:
封装:把客观事物封装成抽象的类,隐藏属性和方法的实现细节,仅对外公开接口。
继承:子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。
多态:接口的多种不同的实现方式即为多态。同一操作作用于不同的对象,产生不同的执行结果。在运行时,通过指向基类的指针或引用来调用派生类中的虚函数来实现多态。
面向对象优点:
封装可以隐藏实现细节,使得代码模块化;
继承可以扩展已存在的类。它们的目的都是为了---代码重用。
而多态则是为了实现另一个目的---接口重用。
面向对象时代吗有更好的内聚性和更低的耦合性,使不同的对象之间减少依赖性,从而达到代码的可复用性,提高编程效率。。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.