javascript 学习笔记

javascript 学习笔记

liohi

代码段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 一个script标签就是一个代码段-->
<!-- 每一个代码段之间相互独立,如果上面代码段报错,不会影响下面代码段-->
//第一个代码段
<script>
var a=1;
console.log(a); //输出a;

//使用了未声明的变量,报错 c is not defined
console.log(c); //报错

//ReferenceError 引用错误
//对于引用错误来说,统统一个代码段如果报了引用错误,错误下面代码就停止执行
console.log(a);


</script>

//第二个代码段
<script>
var b=2;
console.log(b);

console.log(a); //上面代码段定义的数据,下面代码段也可以使用,但是下面代码段定义数据不可以在上面使用
</script>

预编译(预解析)

JS 代码在执行时可分为两个阶段

  1. 预编译(预解析)
  2. 代码执行,即一行一行执行,等到解析结束之后,才会尽心那个代码执行,也就是说浏览器把 JS 代码进行加工之后再去执行,加工的过程就是预编译

预编译期间做了什么?

  1. 声明提升:var变量提升,但是之提升了声明没有赋值。function函数整体提升。提升到代码段最前面
  2. 如果在函数内部的局部变量,就提升但函数内部的最前面

ps:var 变量只提升声明,函数提升的是函数整体

代码执行产生 EC 和 GO

JS 在内存中主要看内存的栈区和堆区,其中基本数据类型存储在栈区,引用数据类型(函数)存储在堆区,地址存储在栈区

JS 代码主要分两类

全局代码:默认进入script标签就会执行全局代码

函数代码:一个函数就是一个局部函数代码

1
2
3
4
5
6
7
8
9
10
11
<script>
//a和b位于全局代码
var a=1;
var b=2;

//c和d位于局部代码
function fn(){
var c=3;
var d=4;
}
</script>
  • 全局代码执行时,会产生全局执行上下文Execution Context Globle(ECG)
  • 每调用函数时就产生一个局部的执行上下文
  • 执行上下文产生即放入一个栈空间:执行上下文栈Execution Context Stack(ECS)
  • 当函数调用完毕,函数的EC出栈,当ECG执行完毕,ECG也要出栈

执行上下文EC的作用:给代码提供数据,代码中需要的数据都从EC中的栈中读取

JS 代码在执行时。会在堆内存中创建一个全局对象Global Object(GO),这个GO也就是Window,是一个全局对象,对象也就是属性的无序集合。很多API都是Window的属性,例如:Date,Array,String,Number,SetTimeout...

  • 执行全局代码时会在ECS中产生ECG ,在堆内存则会有一个GO(Window)

  • 代码执行前ECG中包含两个部分:

    • 变量对象VO(此时就等同于GO
    • 全局代码执行
  • 调用函数fn()时,产生一个EC(fn)入栈,包含三个部分

    • 解析函数称为AST树结构时,会创建一个活动对象Activation Object(AO),AO中包含形参、arguments、函数定义和指向函数对象、定义的变量

    • 作用域链scopchain:由VO(在函数中就是AO的对象)和父级VO组成,查找时会逐层查找。ps:所谓作用域链就是数据在EC中查找的过程,找一个数据,先在自己的EC中找,如果找不到就去父的EC中找,直到ECG,如果还找不到就报错。

    • this绑定的值

1
2
3
4
5
6
7
<script>
//代码执行前存在GO:包含内置属性和自定义的全局变量和全局函数 var n=1;
console.log(n); //去执行上下文(ECG)中寻找n,ECG中有VO,也就是GO,GO中有n
console.log(window.n); //直接去GO中找n m=2; console.log(m);
//ECG中找m,GO中有m console.log(x); //ECG中找不到x,x is not define
console.name(name); //虽然没有自定义name变量,但是GO中内置属性有name
</script>

再来个栗子

1
2
3
4
5
6
7
8
9
10
11
<script>
//代码执行产生ECS
var a=1; //全局代码执行产生ECG,ECG里有VO,也就是GO,a存入到栈区
var b=2; //b和a同样存入到栈区

funtion fn(){ //fn函数体存在堆区,函数地址存在栈区
console.log(">>>>>>>");
}

var arr=["1","1","2"]; //数组与函数类似,地址存在栈区,数组体存在堆区
var obj={name:"老八",age:20}

img1变量和闭包

变量

加 var 和不加 var 的变量区别

需要注意的是:

  • var的变量在预编译的期间会提升,但是不加var的变量在预编译的时候不会提升
  • 不管是否加var,只要是全局变量,在非严格模式下都会挂载到GO
  • var的变量既可以做全局变量也可以局部变量,不加var只能全局变量

平时做项目基本不用var,更不要不定义变量类型

1
2
3
4
5
6
7
<script>
console.log(a);
//虽然有变量提升,但是var只提升了定义,并没有赋值所以应该是undefined var a=1;
console.log(a); //此时a被赋值为1所以应该输出1 console.log(window.a);
//a挂载到GO上,所以在逐层访问的时候能够访问到,下面的b同理 // console.log(b);
//会输出undefined b=2; console.log(window.b);
</script>

使用 let 声明变量

使用let声明的变量没有提升,或者可以理解为变量提升了,但是没有赋值,也就是不能直接访问

1
2
3
4
<script>
console.log(a); //ReferenceError: Cannot access 'a' before initialization let
a=2;
</script>

let+{}可以形成块级作用域,块级作用域中定义的变量,只能在块中使用

1
2
3
4
5
6
7
8
<script>
if(true){
var a=1;
let b=2;
}
console.log(a); // 1
console.log(b); //Uncaught ReferenceError: b is not defined
</script>

使用let声明的变量,并不会挂载到GO

1
<script>let a=1; console.log(window.a); //undefined</script>

let不能重复声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
let a=1;
let a=1; //报错,a不能重复声明
console.log(window.a);
</script>

<script>
function fn(a){
//形参相当于函数内部定义的局部变量
//VO:AO已经有a,也会报错
//Uncaught SyntaxError: Identifier 'a' has already been declared
let a=1;
}
fn();
</script>

所以项目中声明变量基本使用let,弥补了var声明变量的缺点

使用 const 声明变量(常量)的特点

  • 声明不能被修改
  • 使用const声明变量时,必须赋值,不然会报语法错误
  • const 声明的变量也不会提升
  • const{}也可以形成块级作用域
  • const 声明的变量也不会挂载到GO

总结:在项目中,定义变量用 let,定义常量用 const

例题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
console.log(fn); // 打印undefine
//window.fn(); // 报错fn is not a function
console.log(window.fn); //undefine fn说明挂载到了GO

if("fn"in window){ //满足条件

fn(); //.....
/*
函数位于if条件中,
在最新版本的浏览器中,不会提升整体,只会提升fn函数名
*/
function fn(){
console.log(".....");
}
}
fn(); //.....
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
fn(); //最后一次函数提升是打印5,所以输出5
function fn(){console.log(1);} //函数提升,打印1
fn(); //打印5
function fn(){console.log(2);}//函数再提升,覆盖1,打印2
fn(); //打印5
var fn =function fn(){console.log(3);}//函数名提升,相当于赋值
fn(); //打印3
function fn(){console.log(4);} //函数提升打印4
fn(); //打印3
function fn(){console.log(5);}//函数提升打印5
fn(); //打印3
</script>
1
2
3
4
5
6
7
8
9
10
11
<script>
var a=1;b=2;c=3;
function fn(a){ //相当于函数局部变量a
console.log(a,b,c); //先找函数执行上下文,传入了20则a=20,b=2,c-3
a=100;
b=200;
console.log(a,b,c); //a被赋值为100,b为200,c:4
}
b=fn(20); //b为undefined
console.log(a,b,c); //log: 1 undefined 3
</script>

闭包

先看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
var i = 1;
function A() {
var i = 2;
function x() {
console.log(i);
}
return x;
}
var y = A();
y();
function B() {
var i = 20;
y();
}
B();
</script>

画出部分堆栈图

img2

按照堆栈函数调用来说,当 A 函数调用完毕,ECA 出栈,ECA 中分配的占内存,也就是 i,需要回收。但是,由于 ECG 中的一个 y,引用着这个 0x345 的堆空间,0x345 堆空间中存在着 ECA 中 i 的栈空间,所以说,0x345 这个堆和 ECA 中的 i 是不能被释放。一个不能被回收释放掉的栈空间,叫做闭包,i 会常驻内存,会造成内存空间泄露。

然后再调用函数 y,执行上下文ECy入栈console.log(y),所以再去作用域链里寻找i,作用域链里包含自身VO和父VO,最终在父VO里找到i2 输出。调用之后,出栈销毁。

然后调用函数B,执行上下文ECB,包含VO:AO,i的值为20,然后调用y又产生一个执行上下文ECy2ECy2中只有console.log(i),所以在作用域链中寻找,在父VO中找到i=2

所以最终控制台输出 2 2

闭包:一个不能被回收的栈内存,就可以被成为闭包

作用:

  • 保护 EC 中的变量,外界不能直接访问
  • 可以让我们像使用全局变量一样使用局部变量,延长了变量的生命周期

this

this:字面意思是“这个”的意思

this和书写的位置没有关系,和调用的方式有关系,是产生执行上下文EC的时候动态绑定的

this的绑定规则:

  • 默认绑定
  • 隐式绑定
  • 显式绑定
  • new绑定

默认绑定

独立函数调用就是所谓的默认绑定,独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<script>
function fn(){
console.log(this);
}
fn();//独立函数调用,函数内部的this表示window
</script>

<!---------------------------------------->

<script>
function fn(){
console.log(this); //window
}

function gn(){
console.log(this); //window
fn; //独立函数调用
}

function kn(){
console.log(this); //window
gn(); //独立函数调用
}


kn();//独立函数调用
</script>

<!---------------------------------------->

<script>
let obj = {
name:"老八",
fn:function(){
console.log(this);
}
}

let gn= obj.fn;
gn(); //独立函数调用
</script>

<!---------------------------------------->

<script>
function fun(){
console.log(this);
}

var o={
name:"老八",
fun:fun
}
var gn=obj,fn;
gn(); //独立函数调用
</script>

以上this指向的都是window

隐式绑定

另外一种比较常见的调用方式是通过某个对象进行调用的,他就是它的调用位置中,是通过某个对象发起的函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
function fn() {
console.log(this);
}

let obj = {
name: "老八",
fn: fn
}

//通过obj打点调用,fn中this表示什么,
//就看点前面是什么,点前面是obj,this就是obj
obj.fn();
</script>

// 控制台输出:{name:'老八',fn: f}

显式绑定

JS中,函数有多种角色

  • 普通函数
  • 对象中的方法
  • 对象(属性的无序集合,内部有很多默认属性和方法,call,apply,bind...
  • 类(构造器,构造函数)

call()

  • 显示绑定this
  • fn()执行
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
function fn(num1, num2) {
console.log(this, num1 + num2);
}

let obj = { name: "zy" }

//call()从第二个参数开始,开始传递参数给函数
fn.call(fn, 222, 333);

//独立调用函数 this表示window
fn();
</script>

apply()

apply作用和call一样,传参方式不同,需要把参数放到一个数组

1
fn.apply(obj, [222, 333]);

bind()

作用和call()一样但是不会让函数执行,返回this之后的新函数

1
2
let newFn = fn.bind(obj, 222, 333);
newFn();
显式绑定总结:
  • call fn.call(obj,1,2) 显式绑定this,让fn()执行 ,也能传参
  • apply fn.apply(obj,[1,2]) 显式绑定 this,让fn()执行,参数放到数组中
  • bind fn.bind(obj,1,2) 显式绑定this,返回绑定this后的新函数,也能传参
注意:
1
2
3
4
5
6
7
8
9
10
11
<script>
function fn(){
console.log(this);
}
// String{'hello'}是一个对象,
// 会拿hello包装成一个对象
fn.call("hello");
//一项两种实际上是绑定到了window上
fn.call(undefined);
fn.call(null);
</script>

new 绑定

JS中,函数有多种角色

  • 普通函数
  • 对象中的方法
  • 函数也是对象
  • 函数也是类(构造函数/构造器),通常情况下首字母大写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
//定义一个类,Person,构造器、构造函数
function Person() {
//new在函数内部创建了一个对象
//把函数内的this绑定到了对象上
//函数执行
//返回这个对象(new完后,肯定是得到一个对象)

//this指向对象
this.name = "xdec";
this.age = 100;

}

let p1 = new Person();

//new一个类。得到一个对象
let p2=new Person("xxx",1);
console.log(p1);
//控制台: Person{name:xxx,age:1}

//每new一次就创建出一个新的对象
let p3=new Person("yyy",3);
console.log(p2);
//控制台: Person{name:yyy,age:3}
</script>

this 绑定的优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
setTimeout(function () {
console.log(this); // 定时器中的this表示window
}, 2000);
</script>


<button id="btn">按钮</button>
<script>
let btn = document.getElementById("btn");
btn.onclick = function () {
//监听器中的this表示事件源
console.log(this);
//控制台输出 <button id="btn">按钮</button>
}

</script>
  • 默认绑定的优先级最低
  • 显示绑定的优先级高于隐式绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
function fn() {
console.log(this);
}

let obj = {
name: "xxx",
//显式绑定
fn: fn.bind({ name: "yyy" })
}
//隐式绑定
obj.fn();

//最终控制台输出{name:'yyy'}

</script>
  • new 绑定优先级高于隐式绑定
1
2
3
4
5
6
7
8
9
10
11
12
<script>
let obj = {
name: "xxx",
fn: function(){
console.log(this);
}
}

let res = new obj.fn();

//最终控制台输出 fn{}
</script>
  • new 绑定高于显式绑定
1
2
3
4
5
6
7
8
9
10
<script>
function fn() {
console.log(this);
}

let gn = fn.bind({ name: "xxx" })
// gn(); //{name:'xxx'}

new gn(); //fn{}
</script>
  • new 绑定不能和 call 和 apply 比较
1
2
3
4
5
6
7
8
9
10
<script>
function fn() {
console.log(this);
}

let res = fn.call({ name: "xxx" })
console.log(res);

new res();
</script>

箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script>
var foo = function (nums) {
return nums * 2;
}
</script>

<script>
var foo = (nums) => {
return nums * 2;
}
</script>

// 如果形参只有一个,那么()可以不写
<script>
var foo = nums => {
return nums * 2;
}
</script>

// 如果函数体只有一条语句,那么{}和return 都可以不写
<script>
var foo = nums => nums * 2;
</script>

// 如果函数只有一条语句并且返回一个对象
// 需要将对象()起来
<script>
var foo = a => ({ a: 32131 });
</script>

// 没有形参,()不能省略
<script>
var foo = () => ({ a: 32131 });
</script>

箭头函数中的 this

箭头函数中的 this 需要往上找一层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<script>
var gn = () => {
console.log(this);
}

var obj = {
name: "xxx"
}

//显式绑定
gn.call(obj);
</script>

//控制台输出 window

<script>
let fn = () => {
console.log(this);
}
fn.call("hello");
fn.call({});

//返回的都是window
</script>

<script>
var obj = {
name: "xxx",
fn: () => {
console.log(this);
}
}

obj.fn();
// window
</script>

<script>
setTimeout(() => {
console.log(this);
}, 2000);

// 没用箭头函数的时候指向window
// 使用箭头函数之后还是指向window
</script>

对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<ul>
<li>html</li>
<li>css</li>
<li>js</li>
</ul>

<script>
// 得到一个伪数组,伪数组不是数组,是对象
// {0:"a",2:"b",3:"c"}伪数组
document.getElementsByTagName("li");

let lis = document.getElementsByTagName("li");
console.log(lis[0]);

console.dir(lis[0]);
</script>

一个真实的dom元素,本质就是一个对象,这个对象的属性非常多,操作这个对象,性能就很低,由此JQuery被取代,JQuery操作的就是DOM元素,vue、react操作的就是虚拟DOM元素,虚拟DOM元素的属性灭有那么多

new 的原理

new做了什么:

  • 在构造器内部创建一个新的对象
  • 这个歌对象的prototype属性会被复制为该构造函数的prototype属性
  • 让构造器中的this指向这个对象
  • 执行构造器中的代码
  • 如果构造器中没有返回对象,则返回上面创建出来的对象
1
2
3
4
5
6
7
8
9
10
11
//函数在JS中有多种角色,其中一种角色就是构造器

function Fn() {
console.log("fn...");
//1.创建一个新的对象
//2.让函数中的this绑定到这个新对象上(new绑定)
//3.执行类(函数)中的代码
//4.返回上面的新对象
}

let f = new Fn();

原型和原型链

公有属性和私有属性

只要是一个对象,身上必定有一个属性叫__proto____proto__是属性名,叫隐式原型

1
2
3
4
5
6
7
8
9
/*
obj是一个对象,对象是属性的无序集合,属性又分为两类:
1.私有属性
2.公有属性 沿着__proto__找到的属性都是公有属性
*/

let obj = {
name: "xxx", //私有属性
};
1
2
3
4
5
6
7
8
let obj = {
name: "xxx",
age: 20,
};
//obj. 找hanOwnProperty,自己私有属性没有,就沿着__proto__去公有属性找
//只有找到了这个属性或方法,才能使用这个属性或方法

console.log(obj.hasOwnProperty());

a.b

先去自己的EC(执行上下文)中找a,如果找不到,就去父的EC中找,如果还找不到,就去父的父中找,直到找到ECG(全局执行上下文),如果还找不到,error: a is not defined,整个查找机制就叫做作用域链。

b,先找自己的私有属性,如果找不到,就沿__proto__去公有属性中找,如果公有属性一直找不到,得到undefined,因为查找一个对象上不存在的属性,得到undefined,叫原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = {
name: "xxx",
age: 20,
};
//__proto__对应的值是一个对象,这个对象,叫做原型对象
console.log(obj.__proto__);

//obj.__proto__ 对应的是原型对象
//hasOwnProperty 相对于原型对象来说,是私有属性
obj.__proto__.hasOwnProperty("name");

//原型对象,也是对象,只要是一个对象,身上都有一个__proto__
//如果一直找下去,就找到了null
// console.log(obj.__proto__.__proto__);
in

判断一个属性是否的呼吁某个对象(私有、公有)

1
2
let arr = [];
console.log("push" in arr); //true
hasOwnProperty

判断是否是私有

隐式原型和显式原型

例:
1
2
3
4
5
6
let arr1 = new Array("wc", "xq");
console.log(Array.prototype == arr1.__proto__); //true
console.log(arr1.__proto__.__proto__ == Object.prototype); //true
console.log(arr1.__proto__.__proto__.__proto__); //null

let obj1 = new Object();

原型链

作用域链:是在EC中查找数据的机制

原型链:实在对象上查找属性的机制

例如:
1
2
3
4
5
let p = new Person("xxx", 18);
console.log(p.__proto__.constructor == Person); //true

let num = new Number(100);
console.log(num.__proto__.constructor == Number); //true
总结
  • 一切都是对象
  • 对象是属性的无序集合
  • 属性分为公有属性和私有属性
  • 每个对象身上都有一个__proto__(隐式原型)
  • 每个函数身上都有一个prototype(显式原型)
  • 对象的隐式原型和函数的显式原型,指向一个对象,叫做原型对象
  • 每一个原型对象身上有一个constructor属性指向函数本身
  • 标题: javascript 学习笔记
  • 作者: liohi
  • 创建于 : 2023-04-16 16:55:48
  • 更新于 : 2023-04-19 22:26:59
  • 链接: https://liohi.github.io/2023/04/16/javascript 学习笔记/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
 评论