文章目录
前言
学习JS高级后的总结。这里内容多了些,找个时间在分开细写~
强类型的变量类型是不能改变的,弱类型变量是随需改变的。所以JS是弱类型语言。
1 预解析(预编译)
1.1 代码段
1)一个script标签就是一个代码段;JS代码在执行时,是一个代码段一个代码段执行;
2)代码段和代码段之间是彼此独立的,上面的代码段报错了,不会影响下面的代码段执行;
3)上面代码段不能使用下面代码段定义的数据;
4)下面代码段中可以使用上面的代码段中定义的变量。
<script>
var a = 100;
// 上面代码段不能使用下面代码段定义的数据
console.log(b); // ReferenceError: b is not defined
</script>
<!-- 代码段和代码段之间是彼此独立的,上面的代码段报错了,不会影响下面的代码段执行 -->
<script>
var b = 666;
// 下面代码段中可以使用上面的代码段中定义的变量
console.log(a);
</script>
1.2 预解析(预编译)
JS代码的执行分两个阶段:
JS代码在执行时,分成两个阶段,一个叫预解析(预编译),一个叫执行,预解析(预编译)结束后,才会进入到执行阶段。
什么是预解析?
1)浏览器在执行JS代码的时候会分成两部分操作:预解析以及逐行执行代码
2)也就是说浏览器不会直接执行代码, 而是加工处理之后再执行,
3)这个加工处理的过程, 我们就称之为预解析
预编译期间:
1)把声明提升:加var的变量就是被提升,function声明的函数也会提升,提升到代码段的最前面。
2)函数内部的局部变量,提升到函数体的最前面。
注意:变量的提升仅仅是声明 函数的提升不只提升了声明,也提升赋值。
例1:
<script>
// 下面的代码有2个声明
// 1) 加var的变量a
// 2)使用function 声明了一个函数
console.log(a)
// 赋值操作并不会提升
var a = 110;
console.log(a)
fn();
// function声明的函数提升的是整体
function fn() {
console.log("这是一个fn函数~")
}
</script>
========================预编译后的代码=================================
<script>
// 提升的仅仅是声明 不会提升赋值
var a; // und
// 函数的声明和赋值都提升了
function fn() {
console.log("这是一个fn函数~")
}
// 代码进入到了执行阶段
console.log(a); // und
a = 110; // 仅仅是赋值操作
console.log(a); // 110
fn(); // 函数调用
</script>
例2:
提升的同名变量名或函数名,函数会覆盖变量,函数的优先给是高于变量的优先级的。
在JS中,函数是一等公民
<script>
console.log(value);
var value = 123;
function value() {
console.log("fn value");
}
console.log(value);
</script>
<script>
// 预编译后的代码
// var value;
// 当函数提升后,由于它的名字和变量的名字一样
// 提升后,只会有一个value,这个value就是函数了
// 加var的value就被覆盖了
function value() {
console.log("fn value");
}
// .log(value);
// value = 123; 重新给value覆盖 以前value是函数,现在你把123覆值给了value
// .log(value); 123
</script>
2 执行上下文和数据存储
2.1 全局对象/window对象
全局对象:
叫window 只要我们写的全局变量或在全局中写的函数,都会挂载到window上面。
Globle Object ===> GO 说白了,就是window对象
全局对象上,默认也有很多的东西,alert().....
var a = 110; // 全局变量
b = 666; // 全局变量
function fn() { // 全局函数
console.log("fn...")
}
2.2 执行上下文
代码分类:
全局代码:函数外面的代码都是全局代码。
函数/局部代码:一个函数就是一个局部代码。
EC:Execute Context(执行上下文)
EC(G):Execute Context Globle(全局执行上下文)
EC(Fn()):Execute Context Fn()(局部执行上下文)
ECS:Execute Context Stack(执行上下栈)
GO:Globle Object(全局对象)
AO:Active Object(活动对象)
VO:Variable Object(变量对象)
执行上下文:Execute Context ====> EC
全局代码执行产生ECG,每当调用一个函数,就产生一个函数的EC。每产生一个EC,需要放到ECS,当函数调用完毕,这个EC就是出栈,出栈后的EC,会被销毁,所谓的销毁 指是它们分配的内存空间都要被释放掉。
作用:给当前代码提供数据。
全局执行上下文:
(可放进杯子)
当全局代码执行时,就会产生一个全局的执行上下文,EC(G)。
局部执行上下文:
(可放进杯子)
当函数代码执行时,就会产生一个局部的执行上下文,EC(Fn)。只要调用一个函数,就会产生一个局部执行上下文。调用100个函数,就会产生100个执行上下文。
执行上下文栈:
Execute Context Stack ===> ECS
栈:杯子
JS在执行代码时,肯定先执行全局代码,就会产生EC(G),这个EC(G)就要入栈。
当我们调用一个函数,就会产生一个局部的执行上下文,此时这个局部的执行上下文也要入栈。
当函数调用完毕后,这个EC就要出栈,又进入EC(G),当全局代码执行完后,EC(G)也要出栈。
注:栈:在计算机中, 栈是一种数据结构, 这种数据结构描述了操作数据的一方式。先进后出。
队列:先进先出。
2.3 数据存储
内存分区:
这里只需要掌握两个区,一个是栈区,一个是堆区。
基本数据类型,是存储在栈区的,引用数据类型是存储在堆区,堆区的地址,还是保存在栈区。(引用数据为对象包括:数组,函数,对象...)
对于函数而言:形参相当于函数内部的加var的局部变量。
3 var let const(声明变量的方式和区别)
3.1 加var的变量和不加var的变量有什么区别
1)加var的变量在预编译期间会提升,不加var的变量在预编译期间不会提升。
2)不管有没有加var,创建的变量,都会放到GO中的,也就是可以通过window.xx。
3)加var的变量,可能是全局变量,也可能是局部变量,不加var的变量,只能是全局变量。
4)加var的局部变量,不会作用window的属性
项目中尽量不要使用全局变量。在项目中,不要使用没有加var的变量。
使用var声明变量的缺点不足:
1)提升
2)重复声明 浏览器中有这样一个机制,如果一个变量提升过了,后面遇到同名变量,就不会提升了。
其中:
var a=b=4;等价于var a=4;b=4;//这里的b是全局变量,不会提升!
var a=4,b=4;等价于 var a=4;var b=4;
3.2 使用let声明的变量的特点
1)使用let声明的变量没有提升。
针对这个错误:ReferenceError: Cannot access 'a' before initialization
有人是这样说的:let声明的变量也有提升。但是没有赋值(没有初始化)
就记一种:使用let声明的变量也提升,只不过没有初始化,去使用的时候就报错了。
2)使用let配合{}会形成块级作用域。 在C语言中,就有块级作用域。
在JS中,原本是没有块级作用域。在块中声明的变量,只能在块中使用。
3)使用let声明的变量并不会挂载到GO中
4)使用let声明的变量不能重复声明
需要知道:项目中不要使用var来声明变量,你要声明变量,使用let。
3.3 使用const声明的变量(常量)的特点
1)声明的变量不能修改(常量)
2)声明时必须赋值(初始化) 定义 = 声明+赋值(初始化)
3)const声明的常量也不会提升。
4)const声明的常量配合{} 也会形成块级作用域
5)const声明的常量也不会挂载到GO上。
总结:项目中不要使用var 如果声明变量使用let,如果声明常量,使用const。
4 JS中常见的错误
4.1 语法错误
你写的代码 JS编译器不认识。 代码也没有进行预编译,更没有执行。
解决非常简单 找到你的代码,哪里不符合人家的语法。
常见的IDE,自带语法错误检查。
var a = 1;
function fn(){}
4.2 引用错误
当使用了一个没有定义的数据,可能就会报引用错误 解决:你的数据有没有定义出来。
特点:从引用错误开始,后面的代码就不会执行了。
console.log(b);
4.3 类型错误
数据类型使用不当,你提供的数据类型,并不是JS所需要的数据类型,
解决:检测你的数据的数据类型。
let c = ["hello"];
;(function(){
c(); // c本身是个数组,这里当成函数使用了
})();
4.4 范围错误
在使用容器时,方式不对 容器:array object 特别是,使用长度(容器的大小)。
let arr = new Array(-10);//定义一个数组,里面有10个空间(这里写-10)
console.log(arr);
4.5 逻辑错误
它在控制台中不会错误,结果并不是我们需要的,打印的结果和我们想像的结果不一样。
解决:通过JS断点调试。
强调:再出错,不要问我,先打断点调试。
断点可以通过两个地方打
1)通过代码 debugger bug虫子 debug调试错误。
2)在浏览器的调试工具中打断点。
debugger;//写在哪行表示在哪行终止。此语句用于停止执行 JavaScript,并调用 (如果可用) 调试函数。
形参相当于函数内部的加var的局部变量。
使用let声明变量a let不能重复声明。
5 闭包
在一个函数内部嵌套了一个函数,并且里面函数引用外面函数的变量,这样就会形成闭包。
闭包的前提是函数嵌套,x所应对的栈空间释放不了。
专业一点:一个不会释放内存的栈空间就是一个闭包,闭包是一个没有被释放掉内存的栈空间。
闭包:
一个不能销毁的栈空间(局部EC),就是一个闭包。两大作用:
1)保存 延长闭包中数据的生命周期 所对应的内存一直不会被释放掉;缺点:造成内存泄露。
2)保护 外面想去访问里面的数据,是访问不了的。由于x是函数内部的变量,外面也不能访问,对x有保护作用
合理地使用闭包,项目中有时候,也需要使用到闭包。
例:
var i = 5;
function fn(i) {
return function (n) {
console.log(n+(++i))
}
}
var f = fn(1);//如果函数内没有返回值 这里就是undefined!
fn(2);//没人引用它,所以出栈销毁。
console.log("--------");
fn(3)(4);//后面被引用(4)/fn内部的f()函数引用 形成闭包,执行结束,销毁。
console.log("--------");
fn(5)(6);//后面被引用(6)/fn内部的f()函数引用 形成闭包,执行结束,销毁。
f(7);//前面的结束都销毁了,这里引用第7行形成闭包。
console.log(i)
6 IIFE(立即调用函数表达式)
MDN
IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
;(function fn(){//这个分号是预防前面代码影响立即调用函数表达式;前方如果写对象没有带;就会出错
console.log("fn...");
}())
另外几种写法:
;(function f(){}())
;+function f(){}()
;-function f(){}()
;!function f(){}()
这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
注意:当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。
将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。
7 this
JS中的this:
1)如果this出现在一普通的函数中,你通过window.f调用此函数,那么里面的this就表示window就看前面是谁,那么this就表示谁 。window.f() f中的this就表示window。
function fn() { // this出现在普通的函数中
console.log(this) // 此时this表示window
}
fn()
window.fn()
2)事件绑定中 监听器中的this表示事件源。
let btn = document.getElementsByTagName("button")[0]
btn.onclick = function(){
// 在监听器中,this表示事件源
console.log(this)
}
3)对象是属性的无序集合 在一个对象中也可以能函数 如果一个函数出现在对象中 这个函数叫方法。通过对象打点调用了方法,那么这个方法中的this表示这个对象;就看.
前面是谁,那么this就表示谁。
let obj = {
name: "wangcai",
age: 100,
// this出现在方法中
run: function () { // run叫方法 函数
console.log(this) // this表示当前的对象
console.log("run....") // this代表是谁,你就看.前面是谁,
} // this代表什么谁,只有去调用此方法时,才知道this是谁
}
obj.run() // .前面是obj,代表this是obj
4)在IIFE中,this表示window。
5)在严格模式和非严格模式下,this代表的含义是不一样的。
this指向总结:严格模式和非严格模式
JavaScript严格模式下关于this的几种指向详解
6)死记:别人写的一片代码中有this,谁也不知道this代表什么含义,只能代码执行起来后,才知道this表示什么。
8 面向对象
8.1 什么是面向对象
每个对象都由类定义,可以把类看做对象的配方。
类:
有了类,就可以创建对象 类名都会大写(为了和变量名区分)
对象:
是通过类创建出来的。
一种面向对象语言需要向开发者提供四种基本能力(特征):
1)封装 - 把相关的信息(无论数据或方法)存储在对象中的能力
2)聚集 - 把一个对象存储在另一个对象内的能力
3)继承 - 由另一个类(或多个类)得来类的属性和方法的能力
4)多态 - 编写能以多种方法运行的函数或方法的能力
8.2 JS中创建对象有两种方式
1)通过字面量创建,仅仅是JS中才有,在JAVA中,对象都是new出来的
2)通过new一个类
// 通过new一个类去创建对象
let obj = new Object();
obj.name = "xq";
console.log(obj.name); // {}
// 通过字面量的形式创建对象
let obj2 = {
name:"wc"
}
obj2.age = 110;
console.log(obj2.name)
console.log(obj2.age)
8.3 JS中提供的内置类
// 先有类,通过new就可以创建一个对象
// Number叫类 在JS中也叫构造器 在JAVA中,叫类
// new出来的是对象 对象是存储在堆中
// num1是保存了堆的地址 num1指向那个堆
// 不严谨地来说,把num1称为一个对象
let num1 = new Number(110)
// 对象是属性的无序集合
console.dir(num1);
下面的例子中: Object叫类/构造器; obj2叫对象
let obj2 = new Object();//类名都会大写(为了和变量名区分)
obj2.name= "xq";
obj2.age = 666;
// instanceof 判断某个对象是否属于某个类
console.log(obj2 instanceof Object); // true
// Number也是一个类
console.log(obj2 instanceof Number); // false
9 一切都是对象
只有对象才能打点。
对象是属性的无需集合。
基本数据类型也可以是对象:
let str = "hello";
str.toUpperCase()//str会瞬间包装成一个对象
str.length;// str会瞬间包装成一个对象
let num = 110;
num.toFixed(2);//num会瞬间包装成一个对象
10 对象是属性的无需集合
操作集合中的属性,说到操作,就是指"增删改查"。
10.1 查(遍历对象、数组)
如何访问对象中的属性。在JS中提供了两种方式,可以访问对象中的方式
1)打点调用.
属性访问运算符 非常常用
2)通过[]调用 目前来说,先了解
当访问一个对象上不存在的属性,得到的undefiend
遍历对象:把对象中每一个属性都获取到 for in来遍历对象。(不能使用for循环来遍历对象)
key 表示对象中所有的属性名 也叫键 key是变量
for(let key in obj){
// 这个地方,只能通过[]来访问
console.log(obj[key]);
}
遍历数组:数组就是一种特殊的对象,键是索引的对象 。
let arr = ["a","b","c"];
// 1)使用for循环来遍历数组
for(let i=0; i<arr.length; i++){
console.log(arr[i]);
}
// 2)使用foreach来遍历数组
arr.forEach(function(item){ // item 表示数组中的每一个元素
console.log(item);
})
// 3)使用for in来遍历数组
for(let key in arr){
console.log(arr[key]);
}
// 4)使用for of来遍历数组 ES6新方法
for(let item of arr){
console.log(item);
}
10.2 增
给对象中增加一个属性:通过 obj.xxx = “xxx”
10.3 改
所谓的修改,就是添加一个同名的属性,后面添加的同名属性,会把前面的覆盖掉
10.4 删
delete运算符 是一个运算符。
let obj = {
name:"wangcai",
age:100
}
delete obj.age; // 删除obj中的age属性
console.log(obj.age); // 相当于访问一个对象上不存在的属性 结果是undefined
11 公有属性和私有属性
console.log()语句打印的是结果,直接显示信息;
console.dir()语句打印的是内容,显示对象的所有属性和方法。
对象是属性的无序集合。
操作集合中的属性:增删改查。
每个对象上面都是一个属性叫__proto__
,它指向一个对象,这个对象叫叫隐式原型对象,简称为原型对象。
let obj = {
name:"wc", // 私有属性
age:100 // 私有属性
}
console.dir(obj);//显示对象的所有属性和方法
属性分类:
1)公有属性:对于obj来说,公有属性是指__proto__
里面的属性。
2)私有属性:指name和age。
hasOwnProperty(): 判断一个属性是否是一个对象的私有属性。
delete运算符,只能删除私有属性,不能删除公有属性。
如果说私有属性和公有属性同名: 调用时,调用的是自己的私有属性。 在一个对象中找一个属性,先在自己的私有属性中找,找不到,才会去公有属性中找。
12 原型对象和原型链
1)每一个
对象
上,都有一个属性,叫__proto__
,它指向了一个对象,这个对象我们叫原型对象
。
2)每一个构造器(类,函数)
,也是对象
,这个对象上,有一个属性,叫prototype
,它也指向一个对象,和__proto__指向同一个对象
,也是原型对象
。
3)每一个原型对象上,都是有一个属性,叫constructor
,它指向此原型对象所对应的构造器
4)原型链:原型链指查找对象上某个属性的查找机制,查找一个对象上的私有属性,先在自己的私有属性中找,找不到,就沿着__proto__去原型对象上找…
5)作用域链:在EC中,查找数据的一个机制。(EC是局部执行上下文)
.
叫属性访问运算符
13 对象是由类(函数)创建的
函数有多个角色:函数,方法,对象,类
角色1:把函数当作是一个普通的函数
function fn() { // 函数声明
console.log("fn...")
} f
n(); // 函数调用
角色2:函数出现在对象,是一个方法
let obj = {
name: "wangcai",
age: 100,
//下面的函数是一个方法
say: function () { // 一个函数出现在对象中, 我们叫它为方法
console.log("say...")
}
} o
bj.say();
角色3:函数也是一个对象
function fn() {
console.log("fn...")
} /
/ fn也可以是一个对象 对象是属性的无序集合
fn.a = 1; // 给对象上添加私有属性 a
fn.b = 2; // 给对象上添加私有属性 b
fn.c = 3; // 给对象上添加私有属性 c
console.dir(fn)
角色4:函数也可以是一个类(这时候推荐首字母大写)
function Person(name) {
this.name = name; // 给对象设置一个私有属性
} // 只要是对象,必定有一个属性叫__proto__
let p1 = new Person("wangcai"); // p1是对象 对象是属性的无序集合
console.dir(p1);
new本意是新的意思,在计算机中,new就是用来创建对象。 只要加new,最终的结果肯定是对象。
14 call apply bind的使用
call,aplly:都可以改变一个函数中的this指向,并且让函数执行。
区别
:传参不一样,apply传参需要传递一个数组。
function say(){
console.log("我是say函数~");
console.log(this.name);
}
let obj1 = { name:"wc" }
let obj2 = { name:"xq" }
// 1)让函数执行 2)改变this指向
// 让say函数中的this指向obj1
say(obj1)//这里没有加call
say.call(obj2); // 让say中的this指向obj2,并且让say执行
传参的区别
//say.call(obj1,参数1,参数2);//如果有参数的话这样写
//say.apply(obj1,[参数1,参数2]);//apply传参需要传递一个数组
凭借apply传参传递数组的特性可以用来计算数组的最大值。
// apply可以让max执行,并且改变max中的this指向
// apply传参正好是传递一个数组
// max中的this不需要考虑
console.log(Math.max.apply({},arr));//arr是数组名,arr前面的参数是谁不重要,仅是为了让它指向object
bind:也可以改变函数中this的指向,但是不会让函数执行,仅仅是改变this指向。返回一个改变了this指向的函数。
15 继承
继承:
有两类:
父类:也叫基类
子类:也叫派生类
好处:可以让子类去继承父类的属性和方法 子类中本来没有,如果继承了父类的,子类就可以使用这些属性和方法了。
15.1 原型继承
特点:
1)继承父类的公有属性和私有属性,作为子类的公有属性
2)核心代码:Son.prototype = new Father();
function Father(a){
this.a = a; // 这是父类的私有属性
}
Father.prototype.getA = function(){ // getA是它的公有属性
console.log(this.a);
}// 此时Father和Son没有任何关系
//子类 派生类 Son中有没有a 可以让Son继子Father中的a
function Son(){
}
// 改变了Son的原型对象 说白了,改变了Son原型
// 让Son继承了Father的公有属性和私有属性,作为Son的公有属性
Son.prototype = new Father();
// 修正原型对象上的contructor指向
Son.prototype.constructor = Son;
let son = new Son();
console.log(son.a);
son.getA(); //
15.2 call继承
特点:
1)继承父类的私有属性,作为子类的私有属性;用的不多 一般情况下,别人私有的,是不想让别人继承。
2)核心代码:Father.call(this);
function Father(){
// 改变的仅仅是此this指向
this.a = 110;
}
Father.prototype.getA = function(){
console.log(this.a);
}
// let f1 = new Father()
function Son(){
// Father是类 是函数 是对象 是方法
// .call 作用:1)改变this指向 2)让Father执行
// 指向谁call的第1个参数 指向this
Father.call(this)
}
let son = new Son();
console.log(son.a);
son.getA();//此时getA还是无法访问,即只能继承
15.3 组合继承
组合继承: call继承+原型继承
特点:
1)继承父类的公有属性和私有属性,作为子类的公有属性 >原型继承
2)继承父类的私有属性,作为子类的私有属性 >call继承
3)父的私有属性被继承了两次,当我们去访问一个属性时,会先访问私有属性
function Father() {
this.a = 110;
}
Father.prototype.getA = function () {
console.log(this.a);
}
function Son() {
// call继承
Father.call(this)
}
// 原型继承
Son.prototype = new Father();
Son.prototype.constructor = Son;
let son = new Son();
console.log(son.a); // 访问a 访问的是son的私有属性
son.getA()
15.4 只继承父类的公有属性作为子类的公有属性
function Father() {
this.a = 110;
}
Father.prototype.getA = function () {
console.log("我是Father的公有属性");
console.log(this.a);
}
// 把Father的原型对象,赋值给Son的原型对象 这样就继承了原型对象中的getA
// 不好,因为Son和Father共享一个原型对象,当通过Son修改了原型对象,也会影响Father的原型对象
// Son.prototype = Father.prototype;
var Fn = function (){}
// 此时Fn和Father共享同一个原型对象 如果你不对Fn的原型对象操作,就会影响到Father的原形对象
Fn.prototype = Father.prototype;
// new Fn(); 创建出来一个新的对象
Son.prototype = new Fn();
function Son() {
}
let son = new Son();
console.log(son.a); // 访问a 访问的是son的私有属性
son.getA()