JavaScript基础
ECMAScript
JavaScript的数据类型有几种?
8种,但是红宝书是7,没有bigint
基本数据类型
Number 数字类型
String 字符串类型
Boolean 布尔类型
Undefined 未定义类型
Null 空类型
Symbol symbol类型
Bigint 大数字类型
引用数据类型
Object 对象类型
undeclared 与 undefined的区别?
undefined:声明了变量,但是没有赋值
undecalared:没有声明变量就直接使用
var name;
console.log(name);//undefined
console.log(age);//undecalared
数组、字符串一些方法
splice 与 slice 的区别?(待完善。。。)
方法 | 参数 | 用法区别 | |
---|---|---|---|
splice | splice(start, num, item1, item2, …) | 从数组中添加或删除元素,然后返回被删除的数组元素,会改变原始数组 | 1个参数的位置开始一直截取到最后 ; |
slice | slice(start, end) | 从已有的数组中返回你选择的某段数组元素,不会改变原始数组(左闭右开) |
substr 和 substring 的区别?
方法 | 参数 |
---|---|
substr | substr(start,length) |
substring | substring(start,end) |
includes 比 indexOf好在哪?
includes可以检测NaN
,indexOf不能检测NaN
,includes内部使用了Number.isNaN
对NaN
进行了匹配
判断变量类型的方式
typeof
能判断string、number、undefined、boolean、function(返回function)、object(null是object)
适合判断一个变量是否是字符串、数值、布尔值、undefined
instanceof
适合判断引用类型
Object.prototype.toString.call()
Object.prototype.toString.call(b)//[object Number]
能判断大部分数据类型
类型转换
转化为String
方法一:
调用被转换数据类型.toString()方法
但是null,undefined不能用这种方法,会报错
var a=12;
``console.log(a.toString());`
可以用toString()的原理是,因为如上例的a是Number()(其实它是原生函数来的,类似构造函数)的一个实例,所以根据原型链继承的内容,就可以知道之所以各种类型可以调用toString()方法,其实是继承自Object.prototype,调用的是Object.prototype.toString方法。因为null和undefined没有原生函数,所以不能调用这个方法。
方法二:
调用String(),括号填转的数据
null->”null”
undefined->”undefined”
方法三(隐式转换)
使用+、-、*、/拼接字符串的方法实现转换效果
var num=15
console.log(num+’ ‘);
补充,其实JSON.stringify也可以算转换,但是其实它用到的还是toString,JSON的具体会在后面展开。
转化为Number
方法一:
Number()函数
字符串转数字,如果是纯属数字,就直接转; 如果有非数字,则 转化为NaN;如果是空串或者空格串,转为0
布尔转数字:true->1,false->0
null->0
undefined->NaN
Symbol无法转换为数字,会报错:Uncaught TypeError: Cannot convert a Symbol value to a number
BigInt会去除n
Number(12n)//12
对象转换为数字,会按照下面的步骤去执行
1.对对象进行Toprimitive操作
2.进行内部操作DefaultValue,检查该值是否有valueof方法,有则返回基本类型值,使用该值进行强制类型转换;没有则使用toString()的返回值进行强制类型转换,都没有则TypeError
var obj={
valueOf:()=>23
}
console.log(Number(obj));//23
parseInt()把字符串里有效的整数读出转为整数
parseFloat()把字符串转为浮点数
如果是非String,先转String,再转整数
注意:console.log(paresInt(‘rem120px’))—->NaN
console.log(paresInt(‘120px’))—->120
因为第一个字符串的第一个字母不是数字
方法三
利用运算符 -、*、/ 隐式转换
console.log('12'-0)
console.log('12'*1)
console.log('12'/1)
转化为Boolean
显式
Boolean()
0/NaN/空字符串/null/undefined 会转化为false,其他均为true, 对象也会转化为true
隐式
隐式转换一般是在有逻辑判断、有逻辑元素符||、&&、!时触发。
||和&&首先会对一个操作数进行条件判断,如果其不是布尔值,就先将其强制转换为boolean类型,然后再进行条件判断。
对于||,如果条件判断结果为true就返回第一个操作数,false就返回第二个。
&&是true就返回第二个操作数,false就返回第一个操作数。
console.log(b||c);//12
console.log(c||d);//23
console.log(d&&c);//null(false返回第一个)
console.log(b&&c);//23(true返回第二个)
和=
和=的区别是,允许在相等比较中进行强制类型转换,而=不允许
==比较
注意的是,NaN不等于NaN,+0等于-0
1.数字和字符串
根据ES5规范,其中一个为数字,一个为字符串,会将字符串转化为数字
var a=42,b="42";
a==b//true
2.布尔值和其他
会将其中的布尔值转化为数字再比较,如果是字符串和布尔值,先将布尔值转化为数字,再讲字符串转数字,再比较。
var a="42",b=false;
a==b //false false-->0 "42"-->42 42!=0
"0"==false//true false-->0 "0"-->0 0==0
3.null和undefined
一个为null,另一个是undefined是true (就是说==中,null和undefined相等)
var a=null,b;
a==b//true
b==null //true
a0 a’’ a==false //都是false
4.对象与非对象
会对对象调用Toprimitive操作
var a=”abc”,b=new Object(a);
a==b//true b通过Toprimitive操作返回的是”abc”
Toprimitive是什么?
原生函数(内建函数)
原生函数是什么?
JavaScript为基本数据类型提供了封装对象,被称为原生函数。类似于构造函数。
有哪些?
String()
Number()
Boolean()
前3个是包装类
Object()
Array()
Function()
Date()
Regxp()
Error()
Symbol()
特点
类似构造函数
var x=new String("abc");
typeof x //object
x instanceof String //true
Object.prototype.toString.call(x) //[object String]
x类似于构造函数String()创建的一个对象
内部属性[[class]]
所有typeof返回值是object的对象都有一个内部属性[[class]],这个属性相应的构造函数对应,可以通过
Object.prototype.toString.call()
来查看
Object.prototype.toString.call([1,2,3]);//[object Array]
Object.prototype.toString.call(undefined);//[object Undefined]
Object.prototype.toString.call(null);//[object Null]
虽然null和undefined没有对应的原生构造函数,但是内部属性还是有Null和Undefined。
Object.prototype.toString.call("abc");//[object String]
Object.prototype.toString.call(12);//[object Number]
Object.prototype.toString.call(true);//[object Boolean]
这几个基本类型值被各自的封装对象自动包装,称为“包装”
封装、拆封
封装
因为基本类型值没有.length等属性和方法,所以需要通过封装对象才能访问,javascript会自动为基本类型值包装一个封装对象
var a="abc";
let x=a.length//3
封装过程
var temp=new String("abc");
var x=a.length
temp=null;
自行封装基本类型值,可以使用Object()函数
var a=Object("abc");
拆封
想到得到封装对象中的基本类型值,用valueOf()或toString()函数
var a=new String("abc");
typeof a.valueOf();//string
typeof a.toString();//string
a.toString();//"abc"
a.valueOf(); //"abc"
原生原型
构造函数都有原型,String.prototype.indexOf()
可以简写成String#indexOf(…)
引用数据类型
object 对象
访问属性
var obj={a:1}
.方式 :obj.a (这种方式属性名要满足标识符规范,比如”obj-a”这样就得用下面这种方式)
[“属性名”]:obj[“a”] (这种要传属性名字符串)
可计算的属性名:
var s="str";
var obj={ [s+"a"]:1};
对象复制
深复制
对于引用类型,实现的是对其的克隆
function f(){`
`console.log('1')`
`}`
`var obj={`
`a:1,`
`fun:f`
`}`
`var newobj=JSON.parse(JSON.stringify(obj));`
`console.log(newobj.fun===f);//false
浅复制
对于引用类型的属性,实现的是对其的引用
var newobj1=Object.assign({},obj);`
`console.log(newobj1.fun===f);//true
Object.assign()方法的一个参数是目标对象,之后可以是多个源对象,它会遍历源对象的所有可枚举的自有键并把它们复制到目标对象
不同的对象在底层都表示为二进制,在javaScript中二进制前三位都为0的话会被判断为object类型,null的二进制表示是全0,自然前三位也为0,所以执行typeof时会返回object
对象属性包括数据属性和访问器属性
数据属性
Object.getOwnPropertyDescriptor 获取属性对应的属性描述符
Object.getOwnPropertyDescriptors
var obj={
a:2
}
console.log(Object.getOwnPropertyDescriptor(obj,"a"));
{
configurable: true//可配置,表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,是否可以把它修改为访问器属性
enumerable: true//可枚举,是否可以用过for...in循环返回
value: 2
writable: true//可写 ,是否可以被修改
}
Object.defineProperty() 添加一个新属性或者修改一个已有属性,虽然一般不用,但是它可以修改对应的属性描述符
Object.defineProperties()
var obj={}
Object.defineProperty(obj,"a",{
value:2,
writable:false,
configurable:true,
enumerable:true
})
obj.a=3;
console.log(obj.a);//2
对象或属性不可变
对象常量:writable:false,configurable:false
禁止扩展:Object.preventExtensions(obj) 禁止一个对象添加新属性并且保留已有属性
密封:Object.seal() 把所有属性标记为configurable:false
冻结:Object.freeze()创建一个冻结对象,调用Object.seal()并且把所有数据writable:false
访问器属性
对应属性描述符
[[Configurable]]:同上
[[Enumerable]]:同上
[[Get]]:获取函数,读取属性时调用,默认值undefined
[[Set]]:设置函数,写入属性时调用,默认值undefined
var obj={
//getter
get a(){
return this._a_;
},
//setter
set a(val){
this._a_=2*val;
}
}
obj.a=2;
console.log(obj.a);
console.log(obj)
对象的创建
工厂模式
构造函数模式
原型模式
遍历
in 遍历属性名
hasOwnProperty 好像也是属性名
for…in 也是遍历属性名,最好只在对象用,数组用 for…of
for…of 遍历属性值
内置对象
Math、Date、RegExp、String、Number、Boolean、Object、Function、Array、Error
var s="1314";//这是一个字面量
console.log(typeof s);//string
console.log(s instanceof String);//false
var s1=new String('520');//这是一个对象
console.log(typeof s1);//object
console.log(s1 instanceof String);//true
实际上,平时在字面量上访问属性或者方法,其实是因为引擎自动把字面量转换成为相应的对象
比如
var str='abc';
console.log(str.length);
实际上会转换为String对象
DOM
操作元素(增删查改)
获取元素 | |
---|---|
querySelector(‘选择器’) | 通过选择器选择一个元素 |
querySelectorAll(‘选择器’) | 通过选择器选择一组元素,是一个数组 |
document.getElementById() | 通过id属性获取一个元素节点对象 |
document.getElementsByClassName() | 根据class属性值获取一组元素节点对象 |
document.getElementsByTagName() | 通过标签名获取一组元素对象,是伪数组 |
document.getElementsByName() | 通过name属性获取一组元素节点对象,伪数组 |
element.getElementById(‘标签名’) | 获取element元素里面的’标签名’的元素(例如ul里面的li) |
获取其他 | |
document.getElementsByTagName(“body”)[0] | 获取body标签 |
document.body | 获取body标签 |
document.documentElement | 获取html根标签 |
document.all | 获取页面中所有的元素 |
事件
事件流
分三个阶段事件捕获、到达目标、事件冒泡
事件捕获
最不具体(最外层)的节点应该最先收到事件,而最具体的节点(最内层)应该最后收到事件。
下面div点击事件顺序是 document->html->body->div
事件冒泡
最具体的节点(最内层)的节点应该最先收到事件,而最不具体(最外层)应该最后收到事件。
就是说事件是沿着DOM树一路向上的。
上例div点击事件顺序是 div->body->html->document
23、绑定点击事件有几种方式?
三种
xxx.onclick = function (){}
<xxx onclick=""></xxx>
xxx.addEventListener('click', function(){}, false)
24、addEventListener的第三个参数是干嘛的?
决定事件是捕获阶段
执行还是冒泡阶段
执行
true
:捕获false
:默认,冒泡
BOM
一些错误
ReferenceError 作用域判别失败
TypeError 表示作用域判别成功了,但是对结果的操作是非法的或不合理的
JavaScript进阶
一 作用域问题
词法作用域
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符
var a=2;//如果注释这一行结果是undefined
function f(){
console.log(a);
}
function f1(){
var a=3;
f();
}
f1();//2
这个例子是一个常见的问题,或许多数人会认为调用f1应该输出的是3,会把这个问题理解成一个作用域链的问题,但是其实不全是,第九行是f函数在f1里面调用,不是在f1里定义,f的上级作用域是全局,所以其实作用域链是f函数作用域–>全局作用域和f1函数作用域–>全局作用于,所以调用f1输出的是3。这就是体现了一个词法作用域的概念,就是它是一个静态的作用域,是编写代码时决定的,跟在哪里调用没关系。
改成如下,作用域就是f函数作用域–>f1函数作用域–>全局作用域
var a=2;//如果注释这一行结果是undefined
function f1(){
var a=3;
function f(){
console.log(a)
}
f()
}
f1();//3
全局作用域
全局作用域就是在程序任何地方都能访问,window对象的内置属性都属于全局作用域
函数作用域
函数作用域指的是,属于这个函数的全部变量可以在函数的范围内使用,但是函数外不行。
function test() {
var x = 1;
if (true) {
var x = 2; // 同样的变量,已重复赋值
console.log(x); // 2
}
console.log(x); // 2
}
调用栈如下
因为变量x处于函数test的执行上下文中,函数test编译后变量x的值首先为undefined,接着执行赋值语句,先赋值为1,之后又赋值为2,所以,执行最后两个输出语句时,输出的值是2
块级作用域
简单理解就是{ }内的作用域就称为块级作用域,比如if、while等括号里。
var a=2;
if(a===2){
let b=2*a;
console.log(b);//4
}
console.log(b);//报错uncaught ReferenceError: b is not defined,如果let改成var就是4
let不会在块级作用域变量提升,var会
{
console.log(a);//报错uncaught ReferenceError: b is not defined,如果let改成var就是undefined
let a=2;
}
例子分析:
//把上面的test例子的x变量用let定义
function test() {
var y=3;
let x = 1;
if (true) {
var a=5;
let z=4;
let x = 2; // 同样的变量,已重复赋值
console.log(x); // 2
}
console.log(x); // 1
console.log(y);//3
}
test的执行上下文:
上面的图是test执行到第8行后的执行上下文,let声明的变量和var的不同,函数内部通过var声明的变量,在编译阶段全都var被存放到变量环境里面了,通过let声明的变量会在编译阶段会被存放到词法环境(Lexical Environment)中,而且不同块是不同是词法环境,if块是单独的一个环境。词法环境是一个栈结构,第9行的输出是if块里的x为2,之后块执行完毕,if块的环境弹出,之后输出的x是底下的1。
一个变量的查找的过程的从词法环境的栈顶到栈底,再从变量环境里从底往上找(如上图的红线)
var、let、const的区别
作用域 | 变量提升 | 重复声明同一变量 | 声明的变量能否改 | 成为window对象的属性 | |
---|---|---|---|---|---|
var | 函数作用域 | 有 | 可以 | 能 | 是 |
let | 块级作用域 | 没有 | 不可以 | 能 | 否 |
const | 块级作用域 | 没有 | 不可以 | 不能 | 否 |
一个常见的问题
for(var i = 0; i < 5; i++){
setTimeout(function(){
console.log(i);
},0);
};//5 5 5 5 5
解决方法
方法一
for(let i = 0; i < 5; i++){
setTimeout(function(){
console.log(i);
},0);
};
方法二
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function () {
console.log(i);
}, 0)
})(i)
};
什么是暂时性死区?
MDN上的定义:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let?retiredLocale=he
https://juejin.cn/post/6983702070293430303
个人感觉不用去纠结let有没有变量提升,变量提升就是一段代码在编译阶段是能够识别到var和let创建的变量的,只会对二者的操作不一样:对var定义的变量初始化为undefined,而let定义的变量仍然处于未初始化状态。
在用let声明某变量之前引用该变量,就会出现暂时性死区。其实就是因为let没有变量提升,而var有。
console.log(abc);//报错 Cannot access 'abc' before initialization
let abc;
变量提升
主要有以下3条规律:
1.变量和函数在内的所有声明都会在执行代码前先被处理,先编译后执行。
var a=2;//相当于 var a;a=2;console.log(a);
console.log(a);//2
console.log(b)//相当于 var b; console.log(b); b=1;
var b=1;//undefined
2.函数声明会提升,但是函数表达式不会
f();//2
function f(){
var a=2;
console.log(a)
}
f1(); //TypeError
var f1=function fun(){ ...}
3.变量和函数名称相同时,函数会先提升,然后才是变量(个人感觉这个结论不太对)
f();//1
var f;
function f(){
console.log(1);
}
f=function(){
console.log(2);
}
如果定义了两个同名的函数,后定义的会覆盖前定义的
总结1:
:::info
所谓的变量提升,是指在JavaScript代码执行过程中,JavaScript引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值undefined。
:::
变量提升的原理
一段JavaScript代码在执行之前需要被JavaScript引擎编译,编译完成之后,才会进入执行阶段
编译阶段
编译的结果是形成执行上下文和可执行代码,
执行上下文是JavaScript执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如this、变量、对象以及函数等。执行上下文中存在一个变量环境的对象(Viriable Environment),该对象中保存了变量提升的内容。
可执行代码就是除了声明外的代码
f()
console.log(a)
var a=1
function f(){
console.log('函数')
}
这段代码在编译时,第3行有var声明的变量,js引擎会在在环境对象中创建一个名为a的属性,并使用undefined对其初始化,第4行有一个通过function定义的函数,所以它将函数定义存储到堆(heap)中,并在环境对象中创建一个f的属性,然后将该属性值指向堆中函数的位置。
在执行阶段的时候函数f执行输出“函数”,执行第二行时,环境对象中a的值为undefined,输出undefined,之后再执行a=1
作用域链
一般情况下,变量取值到创建这个变量的函数作用域去取值,但是如果在当前作用域中没有查找到值,就会想上级作用域去查找,直到查到全局作用域,这么查找的过程形成的链条叫作用域链。
var a=1;
function f(){
var b=2;
function f1(){
console.log(a+b);
}
return f1;
}
var x=f();
x();
二 上下文
全局上下文:
只有一个,其实就是window对象,所有通过var定义的全局变量和函数都会成为window对象的属性和方法,(let、const的顶级声明不会定义在全局上下文中),上下文在其所有代码都执行完毕后会被销毁。
函数执行上下文:
有无数个,每个函数调用都有自己的上下文,当代码执行流进入函数时,函数的上下文被推到一个上下文栈中,当函数执行完毕后,上下文栈会弹出该函数上下文,将控制权返还给之前的上下文栈上。
调用栈
let a=1
function f(a){
let b=2
let res=add(a,b)
return res
}
function add(x,y){
console.log(x+y)
}
f()
这段代码的执行时栈如下:
执行上下文和作用域的区别
区别1
全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时已经确定了,而不是在函数调用时。
全局执行上下文是在全局作用域确定后,js代码马上执行之前创建。
函数执行上下文是在调用函数时,函数体代码马上执行前创建。
区别2
作用域是静态的,只要函数定义好了就一直存在,并且不再发生变化。
执行上下文时动态的,调用函数时创建,调用结束后就会自动释放。
全局执行上下文存在在整个页面的生存周期内,全局执行上下文只有一份
函数执行上下文在函数执行结束之后会被销毁
区别3
执行上下文对象是从属于所在的作用域
全局上下文环境—->全局作用域
函数上下文环境—->对应的函数作用域
二 闭包问题
简单定义
闭包简单来讲就是一个外部函数里面嵌套一个内部函数,内部函数引用了外部函数的变量等内容,外部函数将内部函数作为返回值(其实也未必要以返回值的形式,只要能够把内部函数传递出去就行),这就是一个闭包。
有了闭包,这样就可以在外部函数以外的地方调用内部函数,本来一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包却可以继续保持对该函数作用域的引用,闭包使得函数可以继续访问定义时的词法作用域。
- 优点:使得外部可以访问内部函数等,延长内部函数等的寿命
- 缺点:滥用闭包造成内存泄露
如下为一个简单的闭包:
function outer(){
var a=2;
function inner(){
console.log(a);
}
return inner;
}
let other=outer();
other();//2
本来outer函数执行完之后,它的执行上下文应该是会被销毁的,但是通过inner(outer函数的闭包),在outer函数执行结束后还能打印出a的值
调用栈的变化如下:
其实other函数的执行上下文中并没有变量a,当other函数执行到输入变量a时,其实是从作用域链other函数作用域–>outer函数的闭包–>全局作用域,
使用回调函数也是闭包
function wait(s){
setTimeout(()=>{
console.log(s);
},1000);
}
wait('你好呀');
wait()执行1000毫秒之后,它的内部作用域并不会消失,timer函数依然保持有wait()作用域的闭包
模块化
模块模式需要具备两个条件
1.必须有外部封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
注意:一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。
单例模块化(IIFE立刻执行函数)
var module=(function m(){
var a="cool";
var b=[1,2,3];
function f(){
console.log(a);
}
function f1(){
console.log(b.join('!'));
}
return {
f:f,
f1:f1
}
})();
module.f();//'cool'
module.f1();//1!2!3
三 this指向问题
this是在运行是绑定的,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
默认绑定(this指向全局对象window)
function f(){
console.log(this.a);
}
var a=2;
f();//2
在以上代码中,f()是不带任何修饰的函数引用进行调用的,所以是属于默认绑定
隐式绑定
function f(){
console.log(this.a);
}
var obj={
a:2,
f:f
}
obj.f()//2
这种情况是函数的引用有上下文对象的(obj对象的上下文),所以隐式绑定规则会把函数调用中的this绑定到这个上下文对象
function f(){
console.log(this.a);
}
var obj={
a:22,
f:f
}
var obj1={
a:2,
obj:obj
}
obj1.obj.f();//22
引用的是最接近的变量a
注意,将如上要调用的函数赋值给另外变量后,”隐式“会消失
以下3个例子都属于赋值操作,setTimeout()函数实现也是类似的
例一:
function f(){
console.log(this.a);
}
var obj={
a:22,
f:f
}
var other=obj.f;
var a=123
other();//123
例二:
function f(){
console.log(this.a);
}
function fun(func){
func();
}
var obj={
a:22,
f:f
}
var a=123
fun(f);//123
相当于将f赋值给func
例三:
function f(){
console.log(this.a);
}
var obj={
a:22,
f:f
}
var a=123
setTimeout(obj.f,100);//123
显式绑定
指定某个对象为函数调用时的this,call、apply、bind函数
call
function f(){
console.log(this.a);
}
var obj={
a:22
}
var other=function(){
f.call(obj)
}
other();//22
setTimeout(other,100);//22
apply
function f(b){
console.log(this.a,b);
return this.a+b;
}
var obj={
a:22
}
var other=function(){
return f.apply(obj,argumments)//argumments是固定变量,不能改名
}
var res=other(3);//22 3
console.log(res);//25
bind
function f(b){
console.log(this.a,b);
return this.a+b;
}
function bind(f,obj){
return function(){
return f.apply(obj,arguments)
}
}
var obj={
a:22
}
var other=bind(f,obj)
var res=other(3);//22 3
console.log(res);//25
new绑定
所有函数都可以用new来调用,new调用的过程见下文 使用new操作符创建构造函数的实例整个过程?
function f(a){
this.a=a;
}
var other=new f(2);
console.log(other.a);//2
注意
- this四条规则的优先级就不一一细说,new绑定>显式绑定>隐式绑定>默认绑定(可参考《你不知道的javaScript上》91~95页)
- 当call、apply、bind需要忽略this绑定时,可以传入null(但是会改变this绑定,函数中如果使用this会绑定到全局对象),可以用∅。
- 还有一种叫软绑定的(好像很复杂。。。。《你不知道的javaScript上》98 有空再看看吧)
- 特殊的箭头函数,箭头函数中的this指的是其外层作用域.
```javascript
function f(a){
return (a)=>{
//这里面的this是f里的
console.log(this.a);
}
}
var obj1={
a:1
}
var obj2={
a:2
}
var other=f.call(obj1);
other.call(obj2);//1
## 四 构造函数、原型链、继承、类
### 构造函数
#### 什么是构造函数?
其实构造函数和普通函数没有特别的区别,任何普通函数只要使用了new操作符就是构造函数。(一般构造函数名的首字母是大写,普通函数小写),如下
#### 使用new操作符创建构造函数的实例整个过程?
例如上面例子,
1.首先在内存显式创建一个新对象。
2.将新对象的[[prototype]]属性赋值为构造函数的prototype属性。
3.构造函数内部的this赋值为这个新对象(这个就是this指向的第四种情况)
4.执行构造函数内部的代码,给新对象添加属性。
5.如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的对象。
注意,
dog的constructor属性会指向Dog构造函数
Dog的prototype属性和dog的 ** proto ** 指向同一个原型对象。
#### 构造函数存在的问题?
当构造函数里面有定义方法时,创建实例时都会创建方法,而不是指向同一个方法?
解决方法?
实际上
this.sayName=function(){console.log(this.name); }等价于this.sayName=new function(){console.log(this.name);}
所以每个实例的方法都不是指向同一个
这样就可以了,this指向是第二种隐式绑定
上面问题用原型解决更好,使用原型对象的好处就是在原型对象上定义的属性和方法都可以被对象共享(所以在原型对象上定义sayName方法就可以实现应用同一个函数)
注意,如果用 下面这样改要加上constructor
[[prototype]]是什么?
所有对象在创建时都会生成一个[[prototype]]属性(包括函数,对象,数组等Object类型),几乎都会被赋予一个非空的值。
介绍一下各种跟原型相关的方法
A.isPrototypeOf(B) 检查A是否是B的原型
Object.getPrototypeOf(B)获取B原型对象
Object.setPrototypeOf(A,B) 把对象B设置为对象A的原型对象
B=Object.create(A) 把对象A设置为对象B的原型
A.hasOwnProperty("属性") A上是否有对应属性,若属性来自原型链则为false
"属性" in 对象 A上是否有对应属性,不管来自本对象还是原型链有就是true
hasPrototypeProperty(A,"属性") 是否只有A的原型有该属性,若A上也有则false
for...in... 遍历对象查找的是整条prototype链(不可枚举的Enumberable:false不可以访问)
Object.keys(A) 遍历A上可枚举的属性包括prototype链
Object.getOwnPropertyNames(A) 遍历A上所有属性无论能否枚举包括prototype链
Object.values() 接收一个对象返回它们内容的数组,对象值的数组
Object.entries() 接收一个对象返回它们内容的数组,键值对数组
[[prototype]]链应该就是原型链。
Dog原型链的整个结构基本如下(但是这个还不是真正的继承)
### 继承(补充优缺点...)
**原型链继承**
举个简单的例子
具体原型链如下图
![image-20220124151130973.png](https://cdn.nlark.com/yuque/0/2022/png/25385691/1657873908278-b85d5c73-72d0-481c-a93e-00f8760c4869.png#averageHue=%23f4f3f3&clientId=u43e0bddf-219c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=459&id=uab9ba526&name=image-20220124151130973.png&originHeight=688&originWidth=1317&originalType=binary&ratio=1&rotation=0&showTitle=false&size=31101&status=done&style=none&taskId=ufb7f76ee-f4c2-4f23-b233-4ee02a5d754&title=&width=878)
其中,实现继承的句子是Sub.prototype=new Super(),实现Sub继承Super
最后,s.getSuper(),通过3步搜索s---->Sub.prototype--->Super.prototype最后一步找到这个方法
补充:因为所有的引用类型都继承自Object,所以后面省略的是Object.prototype,Object.prototype的方法有如下,所以自定义的引用类型都有以下这些方法。
判断继承关系
1. A instanceof B A是否是B的实例(是 true,否 false)
2. A.prototype.isPrototypeOf(B) A是否是B的原型
存在的问题
1.所有继承的属性和方法都会在对象实例间共享,无法做到实例私有(就是一个实例改变了原型链上的属性或方法,其他实例也会改变)
2.子类型在实例化时不能给父类型的构造函数传参
盗用构造函数
利用call或apply方法和改变this指向,实现继承
优点:解决属性共享问题和向父类构造函数传参问题
缺点:必须在构造函数中定义方法,函数不能重用。子类也不能访问父类原型上定义的方法。
组合继承(最流行的方法)
使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性(就是方法在原型链上可以重用,属性用盗用构造函数各个实例可不同)
缺点:有效率问题,会对父类构造函数调用两次,一次是创建子类原型时,二次是在子类构造函数中调用。
原型式继承
object函数的原型链图
这种继承适合有一个对象,想在它的基础上在创建一个新对象。类似于复制一个对象,然后再对其进行添加。
Object.create()和object方法效果相同。
寄生式继承
其实就是对原型式继承的封装而已,缺点是难以重用。
寄生式组合继承(这种应该是最有效的继承方式了)
其实就是通过寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
五.对象有
## 深拷贝和浅拷贝
### 定义
深拷贝:拷贝的是整个变量,对于引用变量实现的是完全克隆,与原来变量完全没有联系
浅拷贝:只拷贝第一层,对于引用变量实现的是地址引用
### 实现方法
**浅拷贝**
1. Object.assign()
2. 展开运算符...
3. Array.prototype.concat()
4. Array.prototype.slice()
let obj={
a:1,
b:{
c:2
}
}
let obj1=Object.assign({},obj);
console.log(obj1.b===obj.b);//true
let obj2={…obj};
console.log(obj2.b===obj.b)//true
let arr=[1,2,{
f:3
}]
let arr1=arr.concat();
console.log(arr1.f===arr.f);//true
let arr2=arr.slice();
console.log(arr2.f===arr.f)
**深拷贝**
1. JSON.parse(JSON.stringify())
2. 递归
```javascript
let obj={
a:1,
b:{
c:2
}
}
let obj4=JSON.parse(JSON.stringify(obj))
console.log(obj4.b===obj.b)//false
//递归待补充.....
异步问题
浏览器中的进程、线程、执行机制
进程与线程的概念具体看操作系统(正在学)
进程中可以有多个线程并发执行
js代码怎么执行
从上往下执行,一行报错后面不执行,先同步后异步
浏览器有多个线程
多个线程互相配合从而渲染出页面,这些线程包括了
- js引擎线程:负责执行JS代码
- GUI线程:主要负责将DOM元素渲染到页面上
- http网络请求线程:负责网络请求
- 实时触发线程:触发定时器setTimeout、setInterval
- 浏览器事件处理线程:onclick,onmouseover
而UI主线程则负责管理以上这些线程
js线程和GUI线程是互斥的(跟微任务和宏任务有关)
JS引擎线程是单线程?
因为多线程会很难管理(比如,一个线程是添加一个Dom元素,另一个线程是删除DOM元素就会产生矛盾,不好处理)
3-5的线程是异步的
这三个线程被触发后并不会立即执行,而是被推入到一个叫任务队列(eventloop)的地方去,
UI线程协调运转各个线程
事件循环/轮询
宏任务(macroTask)和微任务(microTask)
为什么有宏任务、微任务?
为了有些异步任务可以优先执行,不然有新的异步任务只能插入callback queue的队尾。
宏任务:script(整体代码)、setTimeout、setInterval、ajax、DOM事件、MessageChannel、requestAnimationFrame、I/O、UI交互事件
微任务:promise、async/await、MutationObserver(html5)、Object.observe、process.nextTick
为什么宏任务比微任务先执行(这里还要再补充。。。)
微任务是ES6语法规定的,而宏任务是由浏览器规定的
微任务执行过程
完整执行过程
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="select"></div>
<script>
console.log('123');
const select=document.getElementById("select");
const p1="<p>DOM 渲染</p>";
select.innerHTML=p1;
//微任务
Promise.resolve().then(()=>{
alert('微任务。。。。');
})
setTimeout(()=>{
alert("宏任务。。。。");
})
</script>
</body>
</html>
//123
//微任务
//DOM渲染
//宏任务
有关单线程的,执行栈、任务队列、同步任务、异步任务
(涉及一些操作系统暑假再总结)
let promise=new Promise((resolve,reject)=>{
resolve(‘success’) promise.then(resolve=>{}).catch(reject=>{})
})
Promise.resolve(‘success’)/Promise.reject(‘fail’)
promise.all([p1,p2]).then().catch() (p1,p2是两个promise对象,p1,p2都成功才then)
promise.race([p1,p2]).then().catch() (p1,p2是两个promise对象,p1,p2先的成功就then)
asyn function…. asyn返回会延迟,会继续执行后面语句,asyn返回后会继续
await function 不延迟,await执行完再继续
什么是柯里化?
柯里化其实是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
function f(a,b){
return a+b;
}
//这个就是简单的柯里化
function f1(a){
return function f2(b){
return a+b;
}
}
f(1,2);//3
f1(1)(2);//3
它的好处?(待补充。。。)
什么是防抖、节流?
可继续补充。。。。
定义 | 场景 | |
---|---|---|
防抖debounce | 频繁去触发一个事件,只发送最后一次 | 1、input框输入时频繁触发事件可加防抖 2.频繁点击按钮提交表单可加防抖 |
节流throttle | 频繁去触发一个事件,但是只能每隔一段时间触发一次 | 1、鼠标不断点击触发,mousedown(单位时间内只触发一次) 2、监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断 |
跨域
V8垃圾回收机制
内存空间
堆和栈
调用栈中的执行上下文中存放了相应的变量环境,其中原始类型数据的数据值是直接保存在栈里,而引用类型保存的是引用地址,其值是存放在堆中。
var a=1
function f(){
console.log("今天是周五~~")
}
栈中数据回收
栈中数据的回收是通过栈顶指针ESP下移实现的
V8垃圾回收器
堆中存放数据的回收就需要用到垃圾回收器了
代际假说
代际假说(The Generational Hypothesis)的内容,这是垃圾回收领域中一个重要的术语,后续垃圾回收的策略都是建立在该假说的基础之上的,所以很是重要。
代际假说有以下两个特点:
- 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
- 第二个是不死的对象,会活得更久。
其实这两个特点不仅仅适用于 JavaScript,同样适用于大多数的动态语言,如 Java、Python 等。
V8是采用分代垃圾回收机制
在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。(新生代的容量比老生代小)
------------------------------------------
| 新生代 | 老生代 | V8分代
------------------------------------------
垃圾回收器工作流程
第一步,标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。
第二步,回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
第三步,做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。
新生代垃圾回收
新生代垃圾回收过程
新生代实行垃圾回收的是副垃圾回收器,使用Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域(from区),一半是空闲区域(to区)。
新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
- 标记对象,留下活动对象,清除非活动对象
- 把存活的活动对象复制到空闲区
- 将对象区域和空闲区域进行翻转,原来对象区域变成空闲区域,空闲区域变成对象区域,循环反复进行。
Scavenge 算法是典型的牺牲空间换时间的方法,只利用了新生代中的对象区,但是每次都需要将存活的对象从对象区域复制到空闲区域,但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。
对象晋升
因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,新生代对象会被移动到老生区中,这就是晋升
晋升条件
对象是否经历过一次Scavenge算法(通过内存地址判断)
To空间的内存占比是否已经超过25%
老生代垃圾回收
主垃圾回收器主要负责老生区中的垃圾回收。除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。
主垃圾回收器是采用标记 - 清除(Mark-Sweep)和标记-整理(Mark-Compact)的算法进行垃圾回收的。Mark-Sweep(标记 - 清除)
顾名思义就是进行标记和清除,
标记,就是遍历堆中的对象,标记存活着的对象
清除,就是清除没有被标记的对象
但是标记清除后会存在许多内存碎片,所以还要进行整理
Mark-Compact(标记-整理)
如果学过操作系统的话应该会懂什么是内存碎片,操作系统内存管理就需要处理内存碎片
就是将存活的对象移动到一段,把空闲的碎片合并
V8垃圾回收存在的问题和解决
全停顿
由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)
这种停顿对新生代的垃圾回收影响并不大,但是对老生代影响就比较明显了,容易造成卡顿
增量标记
为了减小停顿时间带来的影响,V8 引入了增量标记,即将可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样就不会长时间无响应。