数据类型
基本数据类型
string、number、boolean、undefined、null、Symbol
对象数据类型
按照 值类型和引用类型
值类型:基本类型(除了null),复制值后不会影响
引用类型:对象、数组、null、函数,值得复制是一个内存地址
深拷贝
function deepClone(obj){
if(typeof obj !== "object" || obj == null){
// obj不是引用类型,直接返回
return obj
}
// 初始化返回结果
let result
if(obj instanceof Array){
result = []
}else{
result = {}
}
for(let key in obj){
// 保证key不是obj原型上的属性
if(obj.hasOwnProperty(key)){
//进行递归调用
result[key] = deepClone(obj[key])
}
}
return result
}
作用域和作用域链
作用域
定义
作用域是定义的一套规则,用来管理js引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。
分类
- 全局作用域
- 函数作用域
- es6块级作用域
作用域链
作用域和作用域链不同,作用域是一套规则,而作用域链是对作用域的具体实现。
作用域链:由当前环境与上层环境的一系列变量对象组成,保证当前执行环境对有权限的变量和函数的访问。
执行上下文:EC【execution context】
变量对象:VO【variable Object】
作用域链:scopeChain
如下代码:分析执行流程function bar(){
console.log(myName);
}
function foo(){
var myName = "hello foo";
bar();
}
var myName = "hello out"
foo();
执行上面代码,依次创建全局执行上下文 > foo函数执行上下文 > bar函数执行上下文,当bar函数内执行console时,首先会在bar函内的词法环境和变量环境查找,找不到时会根据out的引用进而查找全局执行上下文。
bar函数和foo函数的out都是指向全局上下文,在函数内部使用一个变量,如果函数内部不存在,就会顺着out的引用查找,这个查找链条为作用域链。
为什么bar函数的作用域链会查找到全局执行上下文而不是foo的函数执行上下文呢?
因为在js内的作用域链是由词法作用域决定,词法作用域也称为静态作用域,作用域是代码中函数定义的位置来决定,和调用的位置无关。
块级作用域中变量查找
function bar(){
var myName = "hello";
let test1 = 100;
if(1){
let myName = "world";
console.log(test);
}
}
function foo(){
var myName = "hello foo";
let test = 2;
{
let test = 3;
console.log(myName);
bar();
}
}
var myName = "hello out"
let myAge =10;
let test =1;
foo();
foo执行console打印时,查找test的顺序为如图的编号。在一个函数执行上下文中,先查找词法环境的变量,词法环境是一个小型栈结构,先定义的在下面,后执行。
关于作用域链的例子
分析作用域链
var a = 20;
function test() {
var b = a + 10;
function innerTest() {
var c = 10;
return b + c;
}
return innerTest();
}
test();
以上代码先后创建,全局>函数test>函数innerTest的执行上下文。innerTest的EC包含了三个变量对象,VO(global),VO(test),VO(innerTest)
主要分析innerTest执行上下文
innerTestEC = {
VO: {...}, // 变量对象
scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
}
对比以下打印结果的例子:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
两段代码执行的结果一样,都打印local scope,但是两段代码究竟有哪些不同呢?
答案就是执行上下文栈的变化不一样。
让我们模拟第一段代码:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
让我们模拟第二段代码:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
自由变量
- 一个变量在当前作用域没有定义,但是被使用
- 向上级作用域一层一层依次查找,直到找到为止
- 如果找到全局作用域还没找到,则报错xx is not define
自由变量的值,是在函数定义的时候,向上级作用域查找并确定值。
变量提升带来的问题
使用var定义的变量存在变量提升,如下代码
var myName = "hello";
function showName(){
console.log(myName) // undefined
if(0){ //这是否定条件,无法进入if内部,但是在if内定义的变量会提升到整个函数内
var myName = "world"
}
console.log(myName) // undefined
}
showName();
如果把if语句换成肯定条件
var myName = "hello";
function showName(){
console.log(myName) // undefined
if(1){ //这是肯定条件,进入if内部,在if内定义的变量会提升到整个函数内
var myName = "world"
}
console.log(myName) // world
}
showName();
如果是使用es6的let定义变量,由于let具有块级作用域的作用。
let myName = "hello";
function showName(){
console.log(myName) // hello
if(1){ //这里不论是肯定还是否定,内部定义的变量只能在if块级作用域内部使用,showName函数读取的myName是全局作用域的值
let myName = "world"
}
console.log(myName) // hello
}
showName();
JS如何执行块级作用域let定义的变量
ES6中的let和const定义的变量,具有块级作用域。
function blockVariable(){
var a=1;
let b=2;
{
let b=3;
var c=4;
let d=5;
console.log(a);
console.log(b);
}
console.log(b);
console.log(c);
console.log(d);
}
blockVariable();
第一步:代码编译并创建执行上下文。
函数blockVariable执行上下文,得出如下结论:
- 函数内部通过var定义的变量,在编译阶段会存在变量环境
- 函数内部(非进入块级内部)通过let声明的变量,在编译阶段存放在词法环境(Lexical Environment)。
- 函数块级作用域内,通过let声明的变量此时并没有放词法环境,然后执行第二步
第二步:进入函数内部块级作用域的执行上下文
可以得出如下结论:
- 进入函数内的块级作用域时,作用域块通过let或const声明变量,被存入词法环境的一个独立区域,该区域的变量不影响作用域块外面的变量(作用域外面声明的变量b,和作用域块内声明的b不影响)。
- 在词法环境内,维护一个小型栈结构,栈底是函数最外层的变量。进入一个作用域块后,就创建一个独立区域把该作用域内部的变量压如栈顶,当作用域执行完成后,作用域块的信息从栈顶弹出,这就是词法环境的结构。
第三步:作用域块变量赋值
当执行到console.log(a)时,会按照词法作用域的执行栈从顶层查找并依次进入变量环境中,如果在词法作用域的第一块区域找到,则读取该值不再进行查找。此时a输入结果为变量环境中的值1,b输出词法作用域第一个区域的值3,执行完块级作用域,将词法作用域栈顶弹出,然后接着执行后续代码。
函数内的console.log(b)结果2;
函数内的console.log(c)结果4;
函数内的console.log(d)
词法环境的结构和工作机制,块级作用域通过词法环境的栈结构实现,变量提升通过变量环境实现。
闭包:
通过以下代码分析闭包
function foo(){
var myName = "hello"
let test1 = 1;
const test2 = 2;
var inner = {
getName: function(){
console.log(test1);
return myName;
},
setName: function(newName){
myName = newName;
}
}
return inner;
}
var bar = foo();
bar.setName("setName")
bar.getName()
console.log(bar.getName())
当执行到foo函数内部return inner这行时,调用栈的情况如下,
代码执行到var bar时的调用栈。inner是一个对象,包含getName和setName两个方法,这两个方法都引用了外部变量myName和test1。根据词法作用域规则,内部函数getName和setName可以访问它们外部函数foo的变量,当inner对象返回给全局变量bar时,虽然foo函数执行结束,foo的执行上下文从栈弹出,但是getName和setName函数依然可以使用foo函数的变量myName和test1.
foo函数执行完成,foo执行上下文从栈顶弹出,但由于返回的setName和getName方法使用了foo内部变量myName和test1,这两个变量依然保存在内存。像给setName和getName加一个专属背包,无论在哪里调用setName和getName,它们都会带着foo函数的专属背包,这个背包除了setName和getName外,其他任何地方都无法访问到,把变量myName和test1这个背包称为foo函数的闭包。
在js中根据词法作用域的规则,内部函数总是可以访问外部函数中声明的变量,当通过调用一个foo外部函数返回内部函数inner后,即使外部函数执行结束,但内部函数引用的变量依然在内存中,把这些变量的集合称为闭包。
当执行bar.setName方法的myName=”setName”时,js引擎沿着当前执行上下文-> foo函数闭包 -> 全局执行上下文的顺序查找myName 变量。调用栈状态图如下:可以看出setName函数执行上下文没有myName变量,foo函数的闭包中包含myName,所以会修改闭包myName变量的值,同理当调用bar.getName时,访问变量myName也位于foo函数闭包中。
闭包存在的两种形式
函数作为返回值使用
function print(){
let num = 100;
return function(){
console.log(num)
}
}
let fn = print()
fn(); // 100
自由变量会在当前定义的作用域,往上级作用域进行查找然后确定值。不是执行的时候确定值
函数作为参数使用
function print(){
let num =200;
fn()
}
let num =100;
function fn(){
console.log(num)
}
print() //100
this
全局执行上下文中的this
函数执行上下文的this
1.通过函数call/apply/bind方法
let bar = {
myName:"sam",
test:90
}
function foo(){
console.log(bar.myName,"before") //sam
this.myName = "北鸟南游";
console.log(bar.myName,"after") //北鸟南游
}
foo.call(bar);
console.log(bar)
观察最后结果,foo函数内部的this已经执行bar对象,通过打印bar对象可以看到bar.myName属性已经为”北鸟南游”。
2.通过对象调用方法设置
let myObj = {
name:"sam",
showName:function(){
console.log(this)
}
}
myObj.showName()
通过myObj对象调用showName方法,则showName的this值执行myObj对象。也可以理解过结果call转化myObj.showName.call(myObj);
如果改变一下调用方式,则this就指向全局变量window
let myObj={
name:"sam",
showName:function(){
this.name="北鸟南游"
console.log(this)
}
}
let foo = myObj.showName;
foo()
此时函数内部的this指向的是全局变量window。在全局环境下调用函数,函数内部this指向的是全局变量window
3.通过构造函数设置
function ChangeThis(){
this.setName = "sam";
}
let myObj = new ChangeThis();
当实例化构造函数,js引擎做了四件事:
1:创建空对象tempObj
2:调用ChangeThis.call方法,将tempObj作为call方法的参数,当ChangeThis执行上下文创建时,它的this指向tempObj对象。
3:执行ChangeThis函数,此时ChangeThis函数指向上下文的this执行了tempObj对象;
4:返回tempObj对象;
用代码实现一个构造函数创建的过程
// 模拟构造函数创建1
function likeNew(){
let tempObj = new Object();
let Con = Array.prototype.shift.call(arguments);
tempObj.__proto__ = Con.prototype;
let result = Con.apply(tempObj, arguments)
return typeof result === "object" ? result : tempObj;
}
// 模拟构造函数创建2
function copyNew(){
let Con = Array.prototype.shift.call(arguments);
let tempObj = Object.create(Con.prototype)
let result = Con.apply(tempObj, arguments);
return result instanceof Object ? result : tempObj;
}
function Person(name){
this.name = name;
}
let p= new Person("sam")
console.log(p)
console.log(likeNew(Person, "sam"))
console.log(copyNew(Person, "sam"))
this经常使用的场景
- 作为普通函数被调用
- 使用call、apply、bind
- 作为对象方法被调用
- 在class方法中调用
- 箭头函数
为了避免使用this出错,应谨记以下3点
- 当函数作为对象的方法调用,函数的this就是该对象
- 当函数被正常调用时,在严格模式下,this的值是underfined,非严格模式下,this执行window
- 嵌套函数内的this不会继承外部函数this的值。
手写bind函数实现
内部依靠apply实现
Function.prototype.bindx=function(){
//将参数拆解为数组
const args = Array.prototype.slice.call(arguments)
//获取this的第一项
const t = args.shift()
const self = this
// 返回一个函数
return function (){
return self.apply(t,args)
}
}