JavaScript高级
1.this
关键字
this
关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。
JavaScript
语言之所以有 this
的设计,跟内存里面的数据结构有关系。
var obj = { foo: 5 };
上面的代码将一个对象赋值给变量obj。JavaScript
引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。
原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。JavaScript
允许在函数体内部,引用当前环境的其他变量。
var f = function () {
console.log(x);
};
上面代码中,函数体里面使用了变量x。该变量由运行环境提供。
现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context
)。所以,this
就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
var f = function () {
console.log(this.x);
}
上面代码中,函数体里面的this.x
就是指当前运行环境的x。
this
使用的场合
- 1.
this
全局环境使用:它指的就是顶层对象window
- 2.
this
可以用在构造函数之中,表示实例对象。
var person = {
name: '张三',
describe: function () {
return '姓名:'+ this.name; // this 就是person对象
}
};
person.describe()
// "姓名:张三"
- 3.
this
在对象的方法:指向就是方法运行时所在的对象
2.call、apply、bind的区别
this
的动态切换,固然为 JavaScript
创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this
固定下来,避免出现意想不到的情况。JavaScript
提供了call
、apply
、bind
这三个方法,来切换/固定this
的指向。
1.Function.prototype.call()
函数实例的call
方法
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
call
方法的参数,应该是一个对象。如果参数为空、null
和undefined
,则默认传入全局对象。
var n = 123;
var obj = { n: 456 };
function a() {
console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456
call方法还可以接受多个参数。
func.call(thisValue, arg1, arg2, ...)
call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数。
function add(a, b) {
return a + b;
}
add.call(this, 1, 2) // 3
上面代码中,call方法指定函数add内部的this绑定当前环境(对象),并且参数为1和2,因此函数add运行后得到3。
2.Function.prototype.apply()
apply
方法的作用与call
方法类似,也是改变this
指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。
func.apply(thisValue, [arg1, arg2, ...])
function f(x, y){
console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
call()和apply()这两个方法都是函数对象的方法,需要通过函数对象来调用。
当函数调用call()和apply()时,函数都会立即执行。
-
都可以用来改变函数的this对象的指向。
-
第一个参数都是this要指向的对象(函数执行时,this将指向这个对象),后续参数用来传实参。
JS提供的绝大多数函数以及我们自己创建的所有函数,都可以使用call 和apply方法。它们的第一个参数是一个对象。因为你可以直接指定 this 绑定的对象,因此我们称之为显式绑定。
例1:
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
// 将 this 指向 obj
foo.apply(obj); //打印结果:2
第一个参数的传递
- 1、thisObj不传或者为null、undefined时,函数中的this会指向window对象(非严格模式)。
- 2、传递一个别的函数名时,函数中的this将指向这个函数的引用。
- 3、传递的值为数字、布尔值、字符串时,this会指向这些基本类型的包装对象Number、Boolean、String。
- 4、传递一个对象时,函数中的this则指向传递的这个对象。
call()和apply()的区别
call()和apply()方法都可以将实参在对象之后依次传递,但是apply()方法需要将实参封装到一个数组中统一传递(即使只有实参只有一个,也要放到数组中)。
var persion1 = {
name: "小王",
gender: "男",
age: 24,
say: function (school, grade) {
alert(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade);
}
}
var person2 = {
name: "小红",
gender: "女",
age: 18
}
call调用
persion1.say.call(persion2, "实验小学", "六年级");
apply调用
persion1.say.apply(persion2, ["实验小学", "六年级"]);
call()和apply()的作用
-
改变this的指向
-
实现继承。Father.call(this)
3.Function.prototype.bind()
bind
方法用于将函数体内的this
绑定到某个对象,然后返回一个新函数。
-
都能改变this的指向
-
call()/apply()是立即调用函数
-
bind()是将函数返回,因此后面还需要加
()
才能调用。 -
bind()传参的方式与call()相同。
2.严格模式
严格模式是从 ES5 进入标准的,主要目的有以下几个。
- 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。
- 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
- 提高编译器效率,增加运行速度。
- 为未来新版本的 JavaScript 语法做好铺垫。
总之,严格模式体现了 JavaScript 更合理、更安全、更严谨的发展方向。
进入严格模式的标志,是一行字符串
use strict
,严格模式可以用于整个脚本,也可以只用于单个函数。严格模式必须从代码一开始就生效
<script>
'use strict';
console.log('这是严格模式');
</script>
<script>
console.log('这是正常模式');
</script>
// 函数严格模式
function strict() {
'use strict';
return '这是严格模式';
}
function strict2() {
'use strict';
function f() {
return '这也是严格模式';
}
return f();
}
function notStrict() {
return '这是正常模式';
}
https://wangdoc.com/javascript/oop/strict.html
3.异步操作概述
单线程模型指的是,JavaScript
只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。
注意,JavaScript
只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript
引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 JavaScript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。JavaScript 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。
如果排队是因为计算量大,CPU 忙不过来,倒也算了,但是很多时候 CPU 是闲着的,因为 IO 操作(输入输出)很慢(比如 Ajax 操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript 语言的设计者意识到,这时 CPU 完全可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript 内部采用的“事件循环”机制(Event Loop)。
单线程模型虽然对 JavaScript 构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果用得好,JavaScript 程序是不会出现堵塞的,这就是为什么 Node 可以用很少的资源,应付大流量访问的原因。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
同步任务(synchronous)和异步任务(asynchronous)
- 同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
- 异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。
任务队列和事件循环【很像Android的消息传递机制】
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)
首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。
异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。
JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。
异步操作的模式:
- 采用回调函数;
- 采用事件监听;
- 发布/订阅;
JavaScript定时器
JavaScript
提供定时执行代码的功能,叫做定时器(timer)
,主要由setTimeout()
和setInterval()
这两个函数来完成。它们向任务队列添加定时任务。
setTimeout
函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。
var timerId = setTimeout(func|code, delay);
setInterval函数的用法与setTimeout完全一致,区别仅仅在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。
// setTimeout
console.log(1);
setTimeout(timeOutPut, 100);
console.log(3);
// 1
//3
//定时器执行100毫秒输出:2
function timeOutPut() {
console.log("定时器执行100毫秒输出:"+2);
}
var count = 0;
var consoleInterval = setInterval(function () {
count++;
console.log("循环输出%d,次数%d", 2, count);
if (count === 10) {
clearInterval(consoleInterval);
}
}, 1000);
循环输出2,次数%count 1
循环输出2,次数%count 2
循环输出2,次数%count 3
循环输出2,次数%count 4
循环输出2,次数%count 5
循环输出2,次数%count 6
循环输出2,次数%count 7
循环输出2,次数%count 8
循环输出2,次数%count 9
循环输出2,次数%count 10
var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);
clearTimeout(id1);
clearInterval(id2);
setTimeout
和setInterval
函数,都返回一个整数值,表示计数器编号。将该整数传入clearTimeout
和clearInterval
函数,就可以取消对应的定时器。
Promise 对象
Promise
对象是JavaScript
的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy)
,充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise
可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。
- 1.
Promise
是一个对象,也是一个构造函数。Promise
设计思想:所有异步任务都返回一个Promise
实例。Promise
实例有一个then
方法,用来指定下一步的回调函数。
function f1(resolve, reject) {
// 异步代码...
}
var p1 = new Promise(f1);
// f1的异步操作执行完成,就会执行f2
p1.then(f2);
Promise
对象通过自身的状态,来控制异步操作。Promise
实例具有三种状态。
- 异步操作未完成(pending)
- 异步操作成功(fulfilled)
- 异步操作失败(rejected)
上面三种状态里面,fulfilled
和rejected
合在一起称为resolved
(已定型)。
var promise = new Promise(function (resolve, reject) {
// ...
if (/* 异步操作成功 */){
resolve(value);
} else { /* 异步操作失败 */
reject(new Error());
}
});
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript
引擎提供,不用自己实现。
resolve
函数的作用是,将Promise
实例的状态从“未完成”变为“成功”(即从pending
变为fulfilled
),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。reject
函数的作用是,将Promise
实例的状态从“未完成”变为“失败”(即从pending
变为rejected
),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise 的优点
- 1.让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。
- 2.Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。
4.作用域和闭包
1.执行上下文
执行上下文主要有两种情况:
- 全局代码: 一段
<script>
标签里,有一个全局的执行上下文。所做的事情是:变量定义、函数声明【在执行全局代码前将window确定为全局执行上下文.
】 - 函数代码:每个函数里有一个上下文。所做的事情是:变量定义、函数声明、this、arguments【
在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中).
】
2.this
this
指的是,调用函数的那个对象。this
永远指向函数运行时所在的对象。
解析器在调用函数每次都会向函数内部传递进一个隐含的参数,这个隐含的参数就是this。
根据函数的调用方式的不同,this会指向不同的对象:【重要】
- 1.以函数的形式调用时,this永远都是window。比如fun();相当于window.fun();
- 2.以方法的形式调用时,this是调用方法的那个对象
- 3.以构造函数的形式调用时,this是新创建的那个对象
- 4.使用call和apply调用时,this是指定的那个对象
需要特别提醒的是:this的指向在函数定义时无法确认,只有函数执行时才能确定。
3.作用域
作用域指一个变量的作用范围。它是静态的(相对于上下文对象), 在编写代码时就确定了。 作用:隔离变量,不同作用域下同名变量不会有冲突。
作用域的分类:
- 全局作用域
- 函数作用域
- 没有块作用域(ES6有了)
if (true) {
var name = 'smyhvae';
}
console.log(name);
上方代码中,并不会报错,因为:虽然 name 是在块里面定义的,但是 name 是全局变量。
全局作用域:直接编写在script标签中的JS代码,都在全局作用域。
在全局作用域中:
- 在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用。
- 创建的变量都会作为window对象的属性保存。
- 创建的函数都会作为window对象的方法保存。
- 全局作用域中的变量都是全局变量,在页面的任意的部分都可以访问到。
变量的声明提前:(变量提升)
使用var
关键字声明的变量( 比如 var a = 1
),会在所有的代码执行之前被声明(但是不会赋值),但是如果声明变量时不是用var
关键字(比如直接写a = 1),则变量不会被声明提前。
举例1:
console.log(a);
var a = 123;
打印结果:undefined
举例2:
console.log(a);
a = 123; //此时a相当于window.a
程序会报错:
image函数的声明提前:
- 使用
函数声明
的形式创建的函数function foo(){}
,会被声明提前。
也就是说,它会在所有的代码执行之前就被创建,所以我们可以在函数声明之前,调用函数。
- 使用
函数表达式
创建的函数var foo = function(){}
,不会被声明提前,所以不能在声明前调用。
很好理解,因为此时foo被声明了,且为undefined,并没有给其赋值function(){}
。
所以说,下面的例子,会报错:
image函数作用域
调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁。
- 每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的。
- 在函数作用域中可以访问到全局作用域的变量,在全局作用域中无法访问到函数作用域的变量。
- 在函数中要访问全局变量可以使用window对象。(比如说,全局作用域和函数作用域都定义了变量a,如果想访问全局变量,可以使用
window.a
)
提醒1:
在函数作用域也有声明提前的特性:
-
使用var关键字声明的变量,是在函数作用域内有效,而且会在函数中所有的代码执行之前被声明
-
函数声明也会在函数中所有的代码执行之前执行
因此,在函数中,没有var声明的变量都会成为全局变量,而且并不会提前声明。
举例1:
var a = 1;
function foo() {
console.log(a);
a = 2; // 此处的a相当于window.a
}
foo();
console.log(a); //打印结果是2
上方代码中,foo()的打印结果是1
。如果去掉第一行代码,打印结果是Uncaught ReferenceError: a is not defined
提醒2:定义形参就相当于在函数作用域中声明了变量。
function fun6(e) {
console.log(e);
}
fun6(); //打印结果为 undefined
fun6(123);//打印结果为123
作用域与执行上下文的区别
区别1:
-
全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
-
全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
-
函数执行上下文是在调用函数时, 函数体代码执行之前创建
区别2:
-
作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
-
执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
联系:
-
执行上下文(对象)是从属于所在的作用域
-
全局上下文环境==>全局作用域
-
函数上下文环境==>对应的函数使用域
作用域链
当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用(就近原则)。如果没有则向上一级作用域中寻找,直到找到全局作用域;如果全局作用域中依然没有找到,则会报错ReferenceError
。
外部函数定义的变量可以被内部函数所使用,反之则不行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
//只要是函数就可以创造作用域
//函数中又可以再创建函数
//函数内部的作用域可以访问函数外部的作用域
//如果有多个函数嵌套,那么就会构成一个链式访问结构,这就是作用域链
//f1--->全局
function f1(){
//f2--->f1--->全局
function f2(){
//f3---->f2--->f1--->全局
function f3(){
}
//f4--->f2--->f1---->全局
function f4(){
}
}
//f5--->f1---->全局
function f5(){
}
}
</script>
</head>
<body>
</body>
</html>
理解:
-
多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
-
查找变量时就是沿着作用域链来查找的
查找一个变量的查找规则:
var a = 1
function fn1() {
var b = 2
function fn2() {
var c = 3
console.log(c)
console.log(b)
console.log(a)
console.log(d)
}
fn2()
}
fn1()
-
在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
-
在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
-
再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
闭包
闭包就是能够读取其他函数内部数据(变量/函数)的函数。只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
上面这两句话,是阮一峰的文章里的,你不一定能理解,来看下面的讲解和举例。
1.如何产生闭包?
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量或函数时, 就产生了闭包。
2.闭包到底是什么?
使用chrome调试查看
-
理解一: 闭包是嵌套的内部函数(绝大部分人)
-
理解二: 包含被引用变量 or 函数的对象(极少数人)
注意: 闭包存在于嵌套的内部函数中。
3.产生闭包的条件
-
1.函数嵌套
-
2.内部函数引用了外部函数的数据(变量/函数)。
来看看条件2:
function fn1() {
function fn2() {
}
return fn2;
}
fn1();
上面的代码不会产生闭包,因为内部函数fn2并没有引用外部函数fn1的变量。
PS:还有一个条件是外部函数被调用,内部函数被声明。比如:
function fn1() {
var a = 2
var b = 'abc'
function fn2() { //fn2内部函数被提前声明,就会产生闭包(不用调用内部函数)
console.log(a)
}
}
fn1();
function fn3() {
var a = 3
var fun4 = function () { //fun4采用的是“函数表达式”创建的函数,此时内部函数的声明并没有提前
console.log(a)
}
}
fn3();
常见的闭包
- 将一个函数作为另一个函数的返回值
- 将函数作为实参传递给另一个函数调用。
闭包1:将一个函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2
f() // 3 //执行fn2
f() // 4 //再次执行fn2
当f()第二次执行的时候,a加1了,也就说明了:闭包里的数据没有消失,而是保存在了内存中。如果没有闭包,代码执行完倒数第三行后,变量a就消失了。
上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。
也就是说,要看闭包对象创建了一个,就看:外部函数执行了几次(与内部函数执行几次无关)。
闭包2. 将函数作为实参传递给另一个函数调用
function showDelay(msg, time) {
setTimeout(function() { //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg
alert(msg)
}, time)
}
showDelay('atguigu', 2000)
上面的代码中,闭包是里面的funciton,因为它是嵌套的子函数,而且引用了外部函数的变量msg。
闭包的作用
-
作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
-
作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
我们让然拿这段代码来分析:
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2;
}
var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2
f() // 3 //执行fn2
f() // 4 //再次执行fn2
作用1分析:
上方代码中,外部函数fn1执行完毕后,变量a并没有立即消失,而是保存在内存当中。
作用2分析:
函数fn1中的变量a,是在fn1这个函数作用域内,因此外部无法访问。但是通过闭包,外部就可以操作到变量a。
达到的效果是:外界看不到变量a,但可以操作a。
比如上面达到的效果是:我看不到变量a,但是每次执行函数后,让a加1。当然,如果我真想看到a,我可以在fn2中将a返回即可。
回答几个问题:
- 问题1. 函数执行完后, 函数内部声明的局部变量是否还存在?
答案:一般是不存在, 存在于闭包中的变量才可能存在。
闭包能够一直存在的根本原因是f
,因为f
接收了fn1()
,这个是闭包,闭包里有a。注意,此时,fn2并不存在了,但是里面的对象(即闭包)依然存在,因为用f
接收了。
- 问题2. 在函数外部能直接访问函数内部的局部变量吗?
不能,但我们可以通过闭包让外部操作它。
闭包的生命周期
-
产生: 嵌套内部函数fn2被声明时就产生了(不是在调用)
-
死亡: 嵌套的内部函数成为垃圾对象时。(比如f = null,就可以让f成为垃圾对象。意思是,此时f不再引用闭包这个对象了)
闭包的应用:定义具有特定功能的js模块
-
将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含n个方法的对象或函数。
-
模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能。
方式一:
(1)myModule.js:(定义一个模块,向外暴露多个函数,供外界调用)
function myModule() {
//私有数据
var msg = 'Smyhvae Haha'
//操作私有数据的函数
function doSomething() {
console.log('doSomething() ' + msg.toUpperCase()); //字符串大写
}
function doOtherthing() {
console.log('doOtherthing() ' + msg.toLowerCase()) //字符串小写
}
//通过【对象字面量】的形式进行包裹,向外暴露多个函数
return {
doSomething1: doSomething,
doOtherthing2: doOtherthing
}
}
上方代码中,外界可以通过doSomething1和doOtherthing2来操作里面的数据,但不让外界看到。
(2)index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>05_闭包的应用_自定义JS模块</title>
</head>
<body>
<!--
闭包的应用 : 定义JS模块
* 具有特定功能的js文件
* 将所有的数据和功能都封装在一个函数内部(私有的)
* 【重要】只向外暴露一个包含n个方法的对象或函数
* 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
var module = myModule();
module.doSomething1();
module.doOtherthing2();
</script>
</body>
</html>
方式二:同样是实现方式一种的功能,这里我们采取另外一种方式。
(1)myModule2.js:(是一个立即执行的匿名函数)
(function () {
//私有数据
var msg = 'Smyhvae Haha'
//操作私有数据的函数
function doSomething() {
console.log('doSomething() ' + msg.toUpperCase())
}
function doOtherthing() {
console.log('doOtherthing() ' + msg.toLowerCase())
}
//外部函数是即使运行的匿名函数,我们可以把两个方法直接传给window对象
window.myModule = {
doSomething1: doSomething,
doOtherthing2: doOtherthing
}
})()
(2)index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>05_闭包的应用_自定义JS模块2</title>
</head>
<body>
<!--
闭包的应用2 : 定义JS模块
* 具有特定功能的js文件
* 将所有的数据和功能都封装在一个函数内部(私有的)
* 只向外暴露一个包信n个方法的对象或函数
* 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->
<!--引入myModule文件-->
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">
myModule.doSomething1()
myModule.doOtherthing2()
</script>
</body>
</html>
上方两个文件中,我们在myModule2.js
里直接把两个方法直接传递给window对象了。于是,在index.html中引入这个js文件后,会立即执行里面的匿名函数。在index.html中把myModule直接拿来用即可。
总结:
当然,方式一和方式二对比后,我们更建议采用方式二,因为很方便。
但无论如何,两种方式都采用了闭包。
闭包的缺点及解决
缺点:函数执行完后, 函数内的局部变量没有释放,占用内存时间会变长,容易造成内存泄露。
解决:能不用闭包就不用,及时释放。比如:
f = null; // 让内部函数成为垃圾对象 -->回收闭包
总而言之,你需要它,就是优点;你不需要它,就成了缺点。
内存溢出和内存泄露
内存溢出
内存溢出:一种程序运行出现的错误。当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误。
代码举例:
var obj = {};
for (var i = 0; i < 10000; i++) {
obj[i] = new Array(10000000); //把所有的数组内容都放到obj里保存,导致obj占用了很大的内存空间
console.log("-----");
}
内存泄漏
内存泄漏:占用的内存没有及时释放。
注意,内存泄露的次数积累多了,就容易导致内存溢出。
常见的内存泄露:
-
1.意外的全局变量
-
2.没有及时清理的计时器或回调函数
-
3.闭包
情况1举例:
// 意外的全局变量
function fn() {
a = new Array(10000000);
console.log(a);
}
fn();
情况2举例:
// 没有及时清理的计时器或回调函数
var intervalId = setInterval(function () { //启动循环定时器后不清理
console.log('----')
}, 1000)
// clearInterval(intervalId); //清理定时器
情况3举例:
<script type="text/javascript">
function fn1() {
var a = 4;
function fn2() {
console.log(++a)
}
return fn2
}
var f = fn1()
f()
// f = null //让内部函数成为垃圾对象-->回收闭包
</script>
5.浅拷贝和深拷贝
浅拷贝
对于对象或数组类型,当我们将a赋值给b,然后更改b中的属性,a也会随着变化。
也就是说,a和b指向了同一块堆内存,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝。
深拷贝
那么相应的,如果给b放到新的内存中,将a的各个属性都复制到新内存里,就是深拷贝。
也就是说,当b中的属性有变化的时候,a内的属性不会发生变化。
其他知识
渲染引擎的主要作用是,将网页代码渲染为用户视觉可以感知的平面文档。
不同的浏览器有不同的渲染引擎。
- Firefox:Gecko 引擎
- Safari:WebKit 引擎
- Chrome:Blink 引擎
- IE: Trident 引擎
- Edge: EdgeHTML 引擎
渲染引擎处理网页,通常分成四个阶段。
解析代码:HTML 代码解析为 DOM,CSS 代码解析为 CSSOM(CSS Object Model)。
对象合成:将 DOM 和 CSSOM 合成一棵渲染树(render tree)。
布局:计算出渲染树的布局(layout)。
绘制:将渲染树绘制到屏幕。
以上四步并非严格按顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。所以,会看到这种情况:网页的 HTML 代码还没下载完,但浏览器已经显示出内容了。
JavaScript 引擎
JavaScript 引擎的主要作用是,读取网页中的 JavaScript 代码,对其处理后运行。
JavaScript 是一种解释型语言,也就是说,它不需要编译,由解释器实时运行。这样的好处是运行和修改都比较方便,刷新页面就可以重新解释;缺点是每次运行都要调用解释器,系统开销较大,运行速度慢于编译型语言。
为了提高运行速度,目前的浏览器都将 JavaScript 进行一定程度的编译,生成类似字节码(bytecode)的中间代码,以提高运行速度。
https://wangdoc.com/javascript/bom/window.html
网友评论