单线程
JavaScript语言是单线程的
区分线程和进程
很多新手是区分不清线程和进程的,没有关系。这很正常。先看看下面这个形象的比喻:
进程是一个工厂,工厂有它的独立资源-工厂之间相互独立-线程是工厂中的工人,多个工人协作完成任务-工厂内有一个或多个工人-工人之间共享空间
如果是 windows 电脑中,可以打开任务管理器,可以看到有一个后台进程列表。对,那里就是查看进程的地方,而且可以看到每个进程的内存资源信息以及 cpu 占有率。
所以,应该更容易理解了:进程是 cpu 资源分配的最小单位(系统会给它分配内存)
进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程是 cpu 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
不同进程之间也可以通信,不过代价较大
现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)
- 、浏览器是多进程的
图中打开了 Chrome 浏览器的多个标签页,然后可以在 Chrome 的任务管理器中看到有多个进程(分别是每一个 Tab 页面有一个独立的进程,以及一个主进程)。
感兴趣的可以自行尝试下,如果再多打开一个 Tab 页,进程正常会 +1 以上(不过,某些版本的 ie 却是单进程的)
注意: 在这里浏览器应该也有自己的优化机制,有时候打开多个 tab 页后,可以在 Chrome 任务管理器中看到,有些进程被合并了(所以每一个 Tab 标签对应一个进程并不一定是绝对的)
- 、为什么JavaScript是单线程的
JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript 不能有多个线程呢 ?这样能提高效率啊。
JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
总结一句话:如果没有遇到条件或者循环,程序按照顺序从上往下依次执
// 同步代码function fun1() {
console.log(1);
}function fun2() {
console.log(2);
}
fun1();
fun2();
// 输出
1
2
很容易可以看出,输出会依次输入1,2,因为代码是从上到下依次执行,执行完fun1(),才继续执行fun2(),但是如果fun1()中的代码执行的是读取文件或者ajax操作,文件的读取和数据的获取都需要一定时间,难道我们需要完全等到fun1()执行完才能继续执行fun2()么?为了解决这个问题,后面我们会介绍同步和异步的概念
- 、定时器
window对象提供了两个方法来实现定时器的效果,分别是window.setTimeout()和window.setInterval。其中前者可以使一段代码在指定时间后运行;而后者则可以使一段代码每过指定时间就运行一次。它们的原型如下:
window.setTimeout(code,millisec);
window.setInterval(code,millisec);
其中,code可以是用引号括起来的一段代码,也可以是一个函数名,到了指定的时间,系统便会自动调用该函数,当使用函数名作为调用句柄时,不能带有任何参数;而使用字符串时,则可以在其中写入要传递的参数。两个方法中的第二个参数是millisec,表示延时或者重复执行的毫秒数。
清除定时器
clearTimeout(定时器名)
clearInterval(定时器名)
- 、同步异步
当变量和函数的声明处在作用域比较靠后的位置的时候,变量和函数的声明会被提升到作用域的开头。
1)为什么会有同步和异步
因为JavaScript的单线程,因此同个时间只能处理同个任务,所有任务都需要排队,前一个任务执行完,才能继续执行下一个任务,但是,如果前一个任务的执行时间很长,比如文件的读取操作或ajax操作,后一个任务就不得不等着,拿ajax来说,当用户向后台获取大量的数据时,不得不等到所有数据都获取完毕才能进行下一步操作,用户只能在那里干等着,严重影响用户体验
因此,JavaScript在设计的时候,就已经考虑到这个问题,主线程可以完全不用等待文件的读取完毕或ajax的加载成功,可以先挂起处于等待中的任务,先运行排在后面的任务,等到文件的读取或ajax有了结果后,再回过头执行挂起的任务,因此,任务就可以分为同步任务和异步任务
(2)同步任务
同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务
(3)异步任务
异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务
function fun1() {
console.log(1);
}
function fun2() {
console.log(2);
}
function fun3() {
console.log(3);
}
fun1();
setTimeout(function(){
fun2();
},0);
fun3();
// 输出
1
3
2
有了异步,就算fun2()里面是文件的读取或ajax这种需要耗时的任务,也不怕fun3()要等到fun2()执行完才能执行啦
(4)异步机制
那么,JavaScript中的异步是怎么实现的呢?那要需要说下回调和事件循环这两个概念啦
首先要先说下任务队列,我们在前面也介绍了,异步任务是不会进入主线程,而是会先进入任务队列,任务队列其实是一个先进先出的数据结构,也是一个事件队列,比如说文件读取操作,因为这是一个异步任务,因此该任务会被添加到任务队列中,等到IO完成后,就会在任务队列中添加一个事件,表示异步任务完成啦,可以进入执行栈啦~但是这时候呀,主线程不一定有空,当主线程处理完其它任务有空时,就会读取任务队列,读取里面有哪些事件,排在前面的事件会被优先进行处理,如果该任务指定了回调函数,那么主线程在处理该事件时,就会执行回调函数中的代码,也就是执行异步任务啦
单线程从从任务队列中读取任务是不断循环的,每次栈被清空后,都会在任务队列中读取新的任务,如果没有任务,就会等到,直到有新的任务,这就叫做任务循环,因为每个任务都是由一个事件触发的,因此也叫作事件循环
总的来说,JavaScript的异步机制包括以下几个步骤
(1)所有同步任务都在主线程上执行,行成一个执行栈
(2)主线程之外,还存在一个任务队列,只要异步任务有了结果,就会在任务队列中放置一个事件
(3)一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面还有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
(4)主线程不断的重复上面的第三步
异步编程
单线程从从任务队列中读取任务是不断循环的,每次栈被清空后,都会在任务队列中读取新的任务,如果没有任务,就会等到,直到有新的任务,这就叫做任务循环,因为每个任务都是由一个事件触发的,因此也叫作事件循环
异步编程主要有:1、setTimeout 2、ajax回调函数 3、promise对象 4、generator函数 5、事件机制
预解析
JavaScript引擎在对JavaScript代码进行解释执行之前,会对JavaScript代码进行预解析,在预解析阶段,会将以关键字var和function开头的语句块提前进行处理。
当变量和函数的声明处在作用域比较靠后的位置的时候,变量和函数的声明会被提升到作用域的开头。
函数提升
func();
function func(){
alert("Funciton has been called");
}
由于JavaScript的预解析机制,上面的代码就等效于:
function func(){
alert("Funciton has been called");
}
func()
变量提升
看完函数声明的提升,再来看一个变量声明提升的例子
alert(a);
var a = 1;
由于JavaScript的预解析机制,上面这段代码,alert出来的值是undefined,如果没有预解析,代码应该会直接报错a is not defined,而不是输出值,不是说要提前的吗?那不是应该alert出来1,为什么是undefined?
所以我们说的提升,是声明的提升。
那么再回过头看,上面的代码就等效于
var a; //这里是声明
alert(a);//变量声明之后并未有初始化和赋值操作,所以这里是 undefined
a = 1
所以变量的提升只是声明的提升
函数同名
通过上一小节的内容,我们对变量、函数声明提升已经有了一个最基本的理解。那么接下来,我们就来分析一些略复杂的情况。
观察下面这段代码
func1();
function func1(){
console.log('This is func1');
}
func1();
function func1(){
console.log('This is last func1');
}
输出结果为
This is last func1
This is last func1
原因分析:由于预解析机制,func1的声明会被提升,提升之后的代码为
function func1(){
console.log('This is last func1');
}
func1();
func1();
同名的函数,后面的会覆盖前面的,所以两次输出结果都是This is last func1。
变量和函数同名
alert(foo);
function foo(){}
var foo = 2;
当出现变量声明和函数同名的时候,只会对函数声明进行提升,变量会被忽略。所以上面的代码的输出结果为
function foo(){}
我们还是来把预解析之后的代码展现出来:
function foo(){};
alert(foo);
foo = 2;
再来看一种
var num = 1;
function num () {
alert( num );
}
num();
代码执行结果为:
Uncaught TypeError: num is not a function
按照常规的书写顺序,同名的函数与变量,变量会覆盖函数
直接上预解析后的代码:
function num(){
alert(num);
}
var num = 1;
num();
预解析是分作用域的
提升原则是提升到变量运行的环境(作用域)中去。
function showMsg()
{
var msg = 'This is message';
}
alert(msg); // msg未定义
还是直接把预解析之后的代码写出来:
function showMsg()
{
var msg;
msg = 'This is message';
}
alert(msg); // msg未定义
函数表达式不会提升
func();
var func = function(){
alert("我被提升了");
};
这里会直接报错,func is not a function,原因就是函数表达式,并不会被提升。只是简单地当做变量声明进行了处理,如下
var func;
func();
func = function(){
alert("我被提升了");
}
作用域
全局作用域
直接编写在 script 标签之中的JS代码,都是全局作用域;
或者是一个单独的 JS 文件中的。
全局作用域在页面打开时创建,页面关闭时销毁;
在全局作用域中有一个全局对象 window(代表的是一个浏览器的窗口,由浏览器创建),可以直接使用。
所有创建的变量都会作为 window 对象的属性保存。
所有创建的函数都会作为 window 对象的方法保存。
局部作用域(函数作用域):
在函数内部就是局部作用域,这个代码的名字只在函数的内部起作用
调用函数时创建函数作用域,函数执行完毕之后,函数作用域销毁;
每调用一次函数就会创建一个新的函数作用域,它们之间是相互独立的。
将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。
例如:
function f1() {
function f2() {
}
}
var num = 456;
function f3() {
function f4() {
}
}
隐式全局变量
声明变量使用`var`, 如果不使用`var`声明的变量就是全局变量( 禁用 )
因为在任何代码结构中都可以使用该语法. 那么再代码维护的时候会有问题. 所以除非特殊原因不要这么用.
下面的代码的错误
function foo () {
var i1 = 1 // 局部
i2 = 2, // 全局
i3 = 3; // 全局
}
作用域及作用域链
全局作用域---全局变量
局部作用域---局部变量---只在当前作用域下有效
块作用域?(js中没有)
只有函数才能产生局部作用域
作用域链的查找规则:
先从当前的作用域中查找,如果有,就返回
如果没有从上一级查找,有就返回,没有继续上一级查找,直到全局
如果全局没有,就报错
4. 对象
(1)、为什么要有对象
function printPerson(name, age, sex....) {
}
// 函数的参数如果特别多的话,可以使用对象简化
function printPerson(person) {
console.log(person.name);
……
}
(2)、什么是对象
现实生活中:万物皆对象,对象是一个具体的事物,一个具体的事物就会有行为和特征。
举例: 一辆车、一部手机、一台电脑、一张桌子
车是一类事物,门口停的那辆车才是对象。特征:红色、四个轮子,行为:驾驶、刹车
(3)、JavaScript中的对象
JavaScript中的对象其实就是生活中对象的一个抽象。
JavaScript的对象是无序属性的集合。
其属性可以包含基本值、对象或函数。对象就是一组没有顺序的值。我们可以把JavaScript中的对象想象成键值对,其中值可以是数据和函数。
Class=”d1”
Key = value
对象的行为和特征
特征---属性
行为---方法
Tips:
事物的特征在对象中用属性来表示。
事物的行为在对象中用方法来表示。
(4)、对象创建方式
对象字面量
var o = {
name: 'zs',
age: 18,
sex: true,
sayHi: function () {
console.log(this.name);
}
};
new Object()创建对象
var person = new Object();
person.name = 'lisi';
person.age = 35;
person.job = 'actor';
person.sayHi = function(){
console.log('Hello,everyBody');
}
工厂函数创建对象
function createPerson(name, age, job) {
var person = new Object();
person.name = name;
person.age = age;
person.job = job;
person.sayHi = function(){
console.log('Hello,everyBody');
}
return person;
}
var p1 = createPerson('张三', 22, 'actor');
自定义构造函数
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayHi = function(){
console.log('Hello,everyBody');
}
}
var p1 = new Person('张三', 22, 'actor');
(5)、属性和方法
1. 如果一个变量属于一个对象所有,那么该变量就可以称之为该对象的一个属性,属性一般是名词,用来描述事物的特征
2. 如果一个函数属于一个对象所有,那么该函数就可以称之为该对象的一个方法,方法是动词,描述事物的行为和功能
(6)、new关键字
构造函数,是一种特殊的函数。主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。
1.、构造函数用于创建一类对象,首字母要大写。
2.、构造函数要和new一起使用才有意义。
new在执行时会做四件事情:
1、new会在内存中创建一个新的空对象
2、new会让this指向这个新的对象
3、new会返回这个新对象
(7)、this详解
JS中this的指向问题,有时会让人难以捉摸,随着学习的深入,我们可以逐渐了解。
函数内部的this几个特点:
1. 函数在定义的时候this是不确定的,只有在调用的时候才可以确定
2. 一般函数直接执行,内部this指向全局window
3. 函数作为一个对象的方法,被该对象所调用,那么this指向的是该对象
4. 构造函数中的this其实是一个隐式对象,类似一个初始化的模型,所有方法和属性都挂载到了这个隐式对象身上,后续通过new关键字来调用,从而实现实例化
(8)、对象的使用
遍历对象的属性
通过for..in语法可以遍历一个对象
删除对象的属性
function fun() {
this.name = 'mm';
}
var obj = new fun();
console.log(obj.name); // mm
delete obj.name;
console.log(obj.name); // undefined
JavaScript错误
当 JavaScript 引擎执行 JavaScript 代码时,会发生各种错误。可能是语法错误,通常是程序员造成的编码错误或错别字。可能是拼写错误或语言中缺少的功能(可能由于浏览器差异)。可能是由于来自服务器或用户的错误输出而导致的错误。当然,也可能是由于许多其他不可预知的因素。
JavaScript try 和 catch
try 语句允许我们定义在执行时进行错误测试的代码块。
catch 语句允许我们定义当 try 代码块发生错误时,所执行的代码块。
JavaScript 语句 try 和 catch 是成对出现的。
在下面的例子中,我们故意在 try 块的代码中写了一个错字。catch 块会捕捉到 try 块中的错误,并执行代码来处理它。
var txt="";
function message() {
try { adddlert("Welcome guest!"); }
catch(err) { txt="本页有一个错误。\n\n";
txt+="错误描述:" + err.message + "\n\n";
txt+="点击确定继续。\n\n";
alert(txt);
} }
finally 语句
finally 语句不论之前的 try 和 catch 中是否产生异常都会执行该代码块。
function myFunction() {
var message, x;
message = document.getElementById("p01");
message.innerHTML = "";
x = document.getElementById("demo").value;
try { if(x == "") throw "值是空的";
if(isNaN(x)) throw "值不是一个数字";
x = Number(x); if(x > 10) throw "太大"; if(x < 5) throw "太小"; }
catch(err) { message.innerHTML = "错误: " + err + "."; }
finally { document.getElementById("demo").value = ""; } }
Throw 语句
throw 语句允许我们创建自定义错误。
正确的技术术语是:创建或抛出异常(exception)。
如果把 throw 与 try 和 catch 一起使用,那么您能够控制程序流,并生成自定义的错误消息。
语法:throw exception
异常可以是 JavaScript 字符串、数字、逻辑值或对象。
本例检测输入变量的值。如果值是错误的,会抛出一个异常(错误)。catch 会捕捉到这个错误,并显示一段自定义的错误消息:
function myFunction() { var message, x; message = document.getElementById("message"); message.innerHTML = ""; x = document.getElementById("demo").value; try { if(x == "") throw "值为空"; if(isNaN(x)) throw "不是数字"; x = Number(x); if(x < 5) throw "太小"; if(x > 10) throw "太大"; } catch(err) { message.innerHTML = "错误: " + err; } }