前端点滴(JS进阶)(三)----倾尽所有
一、作用域链
1. 作用域的概念
作用域就是代码的执行环境,全局执行环境就是全局作用域,函数的执行环境就是局部作用域,它们都是栈内存。
概括来说:
- 局部作用域 ===> 函数执行都会形成一个局部作用域
- 全局作用域 ===> 页面一打开就会形成一个全局作用域
- 局部变量 ===> 在局部作用域里边形成的变量 (通过 var 声明; 函数的形参)
- 全局变量 ===> 在全局作用域形成的变量(var a = 12 或者函数内没有声明,直接赋值的变量)
作用域规则:
- 规则一:函数可以使用函数以外的变量。(作用域链的查找变量)
- 规则二:函数内部,优先使用函数内部的变量。
- 规则三:函数内部也会发生变量提升。
- 规则四:函数内部没有用var声明的变量,也是全局变量。
- 规则五:外部环境不能访问内部环境的任何变量和函数。
实例:
/* 规则一:函数可以使用函数以外的变量。(作用域链的查找变量)。 */
var a = 10;
function fn(){
console.log(a);
}
fn();
//=> 10
/* 规则二:函数内部,优先使用函数内部的变量。 */
var a = 10;
function fn(){
var a = 20;
console.log(a);
}
fn();
//=> 20
/* 规则三:函数内部也会发生变量提升。 */
var a = 10;
function fn(){
console.log(a);
var a = 20;
}
fn();
//=> undefined
/* 规则四:函数内部没有用var声明的变量,也是全局变量。 */
var a = 10;
function fn(){
console.log(a);
}
function fn2(){
a = 20;
}
fn2();
fn();
//=> 20
/* 规则五:外部环境不能访问内部环境的任何变量和函数。 */
function fn(){
var a = 10;
}
fn();
console.log(a); //=> "error" "ReferenceError: a is not defined
2. 作用域链
作用域链实际上就是一种查找方式
在内部函数中查找变量的时候,优先从函数内部自身查找,如果没有查到,则向外层查找,如果外层还没有,则继续向上一层查找,一直查询到全局作用域。这种链式的查找方式就是作用域链。值得注意的是: 函数内部也会发生变量提升。并且严格遵照js自上而下的执行顺序。
先来看一个简单的实例:
/* 实例一 */
var a = 10;
function fn1(){
var a = 20;
function fn2(){
console.log(a);
}
fn2();
a = 30;
console.log(a);
}
fn1();
画个图来表示表示:
所以:输出 20,30
/* 实例二 */
var a = 10;
function fn1(){
function fn2(){
console.log(a);
}
fn2();
a = 30;
console.log(a);
}
fn1();
同样道理:优先从函数内部自身查找,如果没有查到,则向外层查找,如果外层还没有,则继续向上一层查找,一直查询到全局作用域。
输出:10 30
/* 实例三 */
var a = 10;
function fn1(){
function fn2(){
console.log(a);
}
fn2();
var a = 30;
console.log(a);
}
fn1();
函数内部会发生变量提升。输出: undefined 30
二、面向对象编程
面向对象编程就是基于对象的编程。面向对象编程简称OOP(Object-Oritened Programming)为软件开发人员敞开了一扇大门,它使得代码的编写更加简洁、高效、可读性和维护性增强。它实现了软件工程的三大目标:(代码)重用性、(功能)扩展性和(操作)灵活性,它的实现是依赖于面向对象的三大特性:封装、继承、多态。在实际开发中 使用面向对象编程 可以实现系统化、模块化和结构化的设计 它是每位软件开发员不可或缺的一项技能。
1. 知识回顾
JavaScript的基本类型(原始类型、值类型):
(string、number、boolean、undefined、null、symbol)
JavaScript的引用类型(对象类型、引用数据类型):
(String、Number、Boolean、Array、Function、Object、Date、RegExp、Match、Error)
JavaScript中的对象分为:
- 普通对象:直接量语法得到的对象。比如:
var obj = {
name:'张三',
age:20,
say:function(){
console.log("会说法语")
},
sanwei:['100cm', '90cm', '105cm'],
obj2:{name:"李四"}
};
ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含各个类型的值、对象或者函数。
- 内置对象(函数对象):(String、Number、Boolean、Array、Function、Object、Date、RegExp、Match、Error)
普通对象与函数对象的区分:
- 只要是Function的实例,那就是函数对象,其余则为普通对象。
实例:
const obj1 = {};
const obj2 = new Object();
function func1() {
}
const obj3 = new func1();
const func2 = new function() {
}
const func3 = new Function()
分别打印:
console.log(obj1); // object
console.log(obj2); // object
console.log(obj3); // object
console.log(func1); // function
console.log(func2); // object
console.log(func3); // function
2. 定义对象
(1)new 内置对象
之前学习过的String对象、Date对象、Array对象、RegExp对象。使用这些对象的时候,可以new这些函数。然后将得到的返回值当做对象来使用。比如使用字符串对象:
var str = new String('hello world'); // 通过new内置的函数,得到对象。
(2)直接量语法
直接量语法定义的对象,值可以是任何的数据类型:
var obj = {
name:'张三',
age:20,
say:function(){
console.log("会说法语");
},
sanwei:['100cm', '90cm', '105cm'],
obj2:{name:"李四"}
};
注意:直接量语法中的this指向当前对象。
var obj = {
name:'张三',
age:20,
say:function(){
console.log("会说法语,"+this.age+"岁小伙");
},
sanwei:['100cm', '90cm', '105cm'],
obj2:{name:"李四"}
};
obj.say(); //=> "会说法语,20岁小伙"
(3)Es5 new 构造函数
ES5中没有类的概念,只有构造函数或构造器。要想得到对象,只能new一个构造函数。
什么是构造函数?什么是普通函数?
定义函数的时候,正常按照函数的语法来定义即可。如果这个函数正常使用,那么还是一个函数,如果一个函数被new了,那么这个函数就可以叫做构造函数。
function func (){ //func称为构造函数
};
var fn = new func();
console.log( typeof func.prototype); // object
console.log(typeof fn.__proto__); // object
(4)Es6 Class(类)
ES6 引入了class(类),让JavaScript的面向对象编程变得更加简单和易于理解。
class Animal{
constructor(){
this.name ="dog";
this.color ="white";
};
toString(){
console.log('name:'+this.name +',color:'+this.color);
};
}
var animal =new Animal();
animal.toString(); // "name:dog,color:white"
console.log(typeof animal.__proto__); //object
console.log(typeof Animal.prototype); //object
3. 对象的相关操作
参考博客:https://blog.csdn.net/Errrl/article/details/103827729
4. 对象在内存中的存在形式
对象在传值上,是 引用传递。在使用对象的时候,实际上都是使用的对象的地址。
代码:
/* 实例一 */
/* 根据构造函数得到两个对象 */
//定义构造函数
function Person(n, a) {
this.name = n;
this.age = a;
this.say = function () {
console.log(123456);
}
}
//实例化,得到对象
var p1 = new Person('张三', 20);
var p2 = new Person('李四', 25);
得到的两个对象在内存中的形式:
在实际使用对象的时候,实际上都是使用的对象的地址。
/* 实例二 */
//定义构造函数
function Person(n, a) {
this.name = n;
this.age = a;
this.say = function () {
console.log(123456);
}
}
//实例化,得到对象
var p1 = new Person('张三', 20);
var p2 = new Person('李四', 25);
var p3 = p1;
p3.name = '王五';
console.log(p1.name, p3.name); // "王五" "王五"
/* 实例三 */
//定义构造函数
function Person(n, a) {
this.name = n;
this.age = a;
this.say = function () {
console.log(123456);
}
}
//实例化,得到对象
var p1 = new Person('张三', 20);
var p2 = new Person('李四', 25);
var p3 = p1;
p3 = null; //实际上是将p3指向堆区的引用切断,而不会影响到p1
console.log(p1); //=> {name:"张三",age:20,say:f}
把对象当做参数传给函数,实际上传递的也是地址。
/* 实例四 */
//定义构造函数
function Person(n, a) {
this.name = n;
this.age = a;
this.say = function () {
console.log(123456);
}
}
//实例化,得到对象
var p1 = new Person('张三', 20);
var p2 = new Person('李四', 25);
function change(o){
o.name = '王五';
}
change(p1);
console.log(p1.name); //=> "王五"
5. 构造函数
实际上就是一个对象的架构框架。
function fn(name,age){
this.name = name; // 私有属性
this.age = age; // 私有属性
this.say = function(){ //公有属性
console.log("会讲粤语")
}
}
fn.prototype.tellMeAge= function(){
console.log(this.age);
}
var fnc = new fn("yaodao",20);
console.log(fnc.name); //=> "yaodao"
fnc.tellMeAge(); //=> 20
console.log(fnc); //=> {name: "yaodao", age: 20, say: ƒ}
6. 原型对象
(1)没有利用原型对象的情况
在实例化得到一个对象的时候,会为这个对象分配一个原型对象。
不用就等于浪费。
/* 实例 :一个构造函数,实例化得到三个对象。*/
function Person(n, a) {
this.name = n;
this.age = a;
this.say = function () {
console.log(123456);
};
this.cook = function () {
console.log('我做得一手好饭');
};
//....
}
var p1 = new Person('张三', 20);
var p2 = new Person('李四', 25);
var p3 = new Person('王五', 28);
在内存中的形式:
在内存中,会分别为每个对象开辟新的空间。发现每个对象中的say和cook都一样,这样的话,会占用大量的内存。解决办法就是使用原型对象。
(2)获取原型对象
获取原型的方法有两种:__proto__以及 prototype
区别就是:
- 只有函数对象有 prototype 属性,普通对象 没有这个属性。
- 函数对象 和 普通对象 都有 __proto__这个属性。
- prototype 和 __proto__都是在创建一个函数(构造函数)或者对象会自动生成的属性。
(3)利用原型对象
在实例化得到对象的时候,系统会为构造器创建一个对象,该对象会保存构造器的每个实例对象的相同内容,这个对象就是原型对象。所以有了原型对象,再定义构造函数的时候,就可以将每个对象独有的内容放到构造函数中,将每个对象相同的内容都放到原型对象上,以达到节省内存占用的效果。
按照上述方法利用原型对象节省内存的占用。
//构造函数
function Person(n, a) {
this.name = n;
this.age = a;
}
//把say和cook放到Person的原型对象上
Person.prototype.say = function(){
console.log(123456);
};
Person.prototype.cook = function(){
console.log('我做得一手好饭');
};
//实例化三个对象
var p1 = new Person('张三', 20);
var p2 = new Person('李四', 25);
var p3 = new Person('王五', 28);
//测试say和cook是否能正常使用
p1.say(); //=> 123456
p3.cook(); //=> 我做得一手好饭
在内存中的形式:
由上可见,原型对象不能单独存在,肯定要和构造函数产生关系才行,而构造函数又与实例化对象有关系。
所以它们的关系如下:
function fn(){/* 构造函数 */}
var fnc = new fn();
console.log(fn.prototype===fnc.__proto__); //=> true
console.log(fn.prototype.constructor===fnc.constructor); //=> true
7. 原型链
所谓的原型链实际上也是一种查找方式。完整的原型链如下:
那到底怎么查找呢?
找一个对象的属性时:
- 优先从对象自身查找;
- 然后从对象的构造函数中查找;
- 然后从构造函数的原型对象上查找;
- 然后从原型对象的构造函数中查找;
- ….
- 然后就是万物祖宗 Object;
- Object 的原型对象;
- Object 原型对象的原型对象 null;(万物皆空)
先来看看一个实例:
/* 实例一 */
function A() {
this.age = 10;
}
function B(){
this.age = 20;
}
//指定B的原型对象为A的实例
B.prototype = new A();
//实例化B,得到对象
var b = new B();
console.log(b.age);
console.log(b);
输出:
/* 实例二 */
function A() {
this.age = 10;
}
function B(){
}
//指定B的原型对象为A的实例
B.prototype = new A();
//实例化B,得到对象
var b = new B();
console.log(b.age);
console.log(b);
输出:
/* 实例三 */
function A() {
this.age = 10;
}
function B(){
}
B.prototype.age = 20;
//指定B的原型对象为A的实例
B.prototype = new A();
//实例化B,得到对象
var b = new B();
console.log(b.age);
console.log(b);
输出:
/* 实例四 */
function A() {}
function B(){}
B.prototype.age = 20;
//指定B的原型对象为A的实例
B.prototype = new A();
//实例化B,得到对象
var b = new B();
输出:
/* 实例五 */
function A() {
}
function B(){
B.prototype.age = 20;
}
//指定B的原型对象为A的实例
B.prototype = new A();
//实例化B,得到对象
var b = new B();
console.log(b.age);
console.log(b);
输出:
/* 实例六 */
function A() {
this.age = 5
}
A.prototype.age = 10;
A.prototype.age = 15;
function B(){
this.age = 20;
B.prototype.age = 25;
}
B.prototype.age = 30;
//指定B的原型对象为A的实例
B.prototype = new A();
B.prototype.age = 35;
//实例化B,得到对象
var b = new B();
console.log(b.age);
console.log(b);
注意:
A.prototype === a.__proto__
B.prototype === b.__proto__
内存图形式:
输出:
资料参考:https://segmentfault.com/a/1190000015642813
三、闭包
1. JavaScript 垃圾回收机制
(1)JS 的垃圾回收机制的基本原理
找出那些不再继续使用的变量,然后释放其占用的内存,垃圾收集器会按照固定的时间间隔周期性地执行这一操作。
(2)回收方式
标记清除
当变量进入环境时,将这个变量标记为“进入环境”;当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
引入计数(低级浏览器)
另外一种不太常见的垃圾收集策略叫引用计数(Reference Counting),此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加 1,如果该变量的值变成了另外一个,则这个值得引用次数减 1,当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存。
而引用计数的不继续被使用,是因为循环引用的问题会引发内存泄漏。
例如:
function problem() {
var objA = new Object();
var objB = new Object();
objA.someObject = objB;
objB.anotherObject = objA;
}
objA 和 objB 通过各自的属性相互循环引用,也就是说,两个对象的引用次数都是 2。在函数执行完毕后,objA, objB 还将继续存在,因为他们的引用计数永远不会是 0。假如这个函数被多次执行,就会导致大量的内存得不到释放。(内存泄漏)
不仅如此,造成内存泄漏的原因还有很多。
(3)内存泄漏的情况以及解决办法
- 意外的全局变量引起的内存泄露
- 原因:全局变量不会被回收。
- 解决:使用严格模式避免。
- 闭包
- 原因:一、闭包可以维持函数内部变量驻留内存,使其得不到释放。二、活动对象被引用,使闭包内的变量不会被释放
- 解决:减少闭包的使用;将活动对象赋值为null
function showId() {
var app = document.getElementById("app")
app.onclick = function(){
aler(app.id) // 这样会导致闭包引用外层的app,当执行完showId后,app无法释放
}
}
// 改成下面
function showId() {
var app = document.getElementById("app")
var id = app.id
app.onclick = function(){
aler(id)
}
app = null // 主动释放app
}
- 被清理的DOM元素的引用
- 原因: 虽然DOM被删掉了,但对象中还存在对DOM的引用
- 解决: 将对象赋值为null
function click(){
// 但是 button 变量的引用仍然在内存当中。
const button = document.getElementById('button');
button.click();
button = null; // 主动释放button
}
// 移除 button 元素
function removeBtn(){
document.body.removeChild(document.getElementById('button'));
}
removeBtn();
click();
- 定时器未清除
- 原因:定时器内部实现闭包,一直执行回调函数
- 解决:清除定时器,定时器对象赋值为null
2. 什么是闭包?
我们都知道,js的作用域分两种,全局和局部,基于我们所熟悉的作用域链相关知识,我们知道在js作用域环境中访问变量的权利是由内向外的,内部作用域可以获得当前作用域下的变量并且可以获得当前包含当前作用域的外层作用域下的变量,反之则不能,也就是说在外层作用域下无法获取内层作用域下的变量,同样在不同的函数作用域中也是不能相互访问彼此变量的,那么我们想在一个函数内部也有限权访问另一个函数内部的变量该怎么办呢?闭包就是用来解决这一需求的办法之一,闭包的本质就是在一个函数内部创建另一个函数。
首先要清楚闭包的四大特性:
- 函数嵌套函数,内部函数通常被称为闭包函数,外部函数带有内部函数的返回值。
- 函数内部可以引用函数外部的参数和变量
- 可以实现在全局变量下获取到局部变量中的变量的值
- 参数和变量不会被垃圾回收机制回收
3. 使用闭包
那么使用闭包有什么好处呢?
使用闭包的好处是:
- 希望一个变量长期驻扎在内存中
- 避免全局变量的污染
- 私有成员的存在
说先来看看没有使用闭包的情况:
function a(){
var i = 1;
console.log(i++);
}
a(); // 1 执行i++后,变量i被回收
a(); // 1
a(); // 1
a(); // 1
原因就是在调用函数a时,函数执行完毕,变量被释放造成var i = null(被回收机制回收),每次调用实际上只是console.log(null+1);
再来看看使用了闭包函数的情况:
function a(){
var i = 1;
function b(){
console.log(i++);
}
return b;
}
var bb = a();
bb();//1 执行i++后,变量i还在
bb();//2
bb();//3
bb();//4
bb = null;//释放变量i
一般情况下,在函数a执行完后,就应该连同它里面的变量一同被销毁,但是在这个例子中,匿名函数作为a的返回值被赋值给了bb,这时候相当于bb =function(){console.log(i++)},并且匿名函数内部通过原型链引用着a里的变量 i ,所以变量 i 无法被销毁,当程序执行完bb(), 这时候,a 和b 的执行环境才会被销毁。
接下来说说闭包的每个特性:
特性一: 函数嵌套函数,内部函数通常被称为闭包函数,外部函数带有内部函数的返回值。
特性二:函数内部可以引用函数外部的参数和变量。
特性四:参数和变量不会被垃圾回收机制回收。
function foo(x) {
var tmp = 3;
function bar(y) {
alert(x + y + (++tmp));
}
bar(10);
}
foo(2); //16
foo(2); //16
foo(2); //16
不管执行多少次,都会alert 16,因为bar能访问foo的参数x,也能访问foo的变量tmp。
但,这还不是闭包。当你return的是内部function时,就是一个闭包。内部function会close-over(封闭)外部function的变量直到内部function结束。
function foo(x) {
var tmp = 3;
return function (y) {
alert(x + y + (++tmp));
}
}
var bar = foo(2); // bar 现在是一个闭包
bar(10);//16
bar(10);//17
bar(10);//18
bar = null; // 释放变量 tmp
特性三:可以实现在全局变量下获取到局部变量中的变量的值。
var b = 2;
function fn(){
var a = 1;
return function (){
return a;
}
}
var fn2 = fn();
var b = fn2();
console.log(b); //=> 1
4. 闭包实例
/* 实例一:通过作用域链实现对全局变量的自增 */
/* 原因:全局变量不会被回收 */
var a = 1;
function fn(){
a++;
console.log(a);
}
fn();//2
fn();//3
fn();//4
a = null;//释放全局变量
/* 实例二:通过闭包实现对局部变量的自增 */
/* 原因:闭包造成局部变量不被释放 */
var a = 1;
function fn(){
a++;
console.log(a);
}
fn();//2
fn();//3
fn();//4
a = null;//释放全局变量
/* 案例三:通过闭包保存定时器回调函数中的变量 */
for(var i = 0;i<10;i++){
setTimeout(function(){
console.log(i);
},1000)
}
//=> 输出10个10,原因如果清楚Event Loop就清楚setTimeout在执行完
//=> 同步任务后执行,又由于for循环中的变量为全局变量经过循环后i =10
//=> setTimeout*10,所以输出10个10 。
/* 使用了闭包的定时器可以在闭包函数内部保存变量 */
for(var i = 0;i<10;i++){
(function(i){
setTimeout(function(){
console.log(i);
},1000)
})(i)
}
//=> 输出0~9,就是因为闭包函数使得变量不被释放。
//=> 值得注意的是不仅setTimeout,还有事件监听...也同样需要闭包来实现需求。
5. 使用闭包的注意点
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
6. 闭包总结