js基础知识强化

246 阅读9分钟

1.js中的数据类型

数据类型:
  • 基础类型:undefined,Null,String,Symbol,Bigint,Boolean,Number
  • 引用类型:object(Array,Function...)
数据类型检测:
  • typeof:基础类型可以,引用类型除了Function,其他都是object
  • instanceof:判断引用类型
let car = new String('Mercedes Benz')
car instanceof String // true
let str = 'Covid-19'
str instanceof String // false

原理实现:

function myInstanceof(left, right) {
  if (typeof left !== "object" || left === null) {
    return false;
  }
  let proto = Object.getPrototypeOf(left);
  while (true) {
    if (proto === false) return false;
    if (proto === right.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}
console.log(myInstanceof(new Number(123), Number)); // true
  • instanceof:判断引用类型 1.Object.prototype.toString
Object.prototype.toString.call([])       //"[object Array]"

返回格式:[object Xxx]

2.深浅拷贝

  • 深浅拷贝的区别: 浅拷贝和深拷贝都只针对于引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象;
  • 深浅拷贝的区别:

浅拷贝:

1.Object.assign(target,object);

let target={}
let object={
  name:"ghp",
  age:10
}
Object.assign(target,object);
console.log(target);

2.扩展运算符方式:let cloneObj = { ...obj }; 3.Object.create(obj) 4.针对数组:concat、slice

let arr = [1, 2, 3];
let newArr = arr.concat();
let newArr = arr.slice();

实现浅拷贝:

let object={
  name:"ghp",
  age:10
}
const shallowClone=(target)=>{
  if(typeof target==='object'&&target!==null){
    const cloneTarget=Array.isArray(target)?[]:{};
    for (const key in target) {
      if (Object.hasOwnProperty.call(target, key)) {
        cloneTarget[key]=target[key]
      }
    }
    return cloneTarget;
  }else{
    return target;
  }
}
console.log(shallowClone(object));

深拷贝:

  • JSON.stringfy() :把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象
    缺陷: 这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON

4.为什么存在变量提升

先解释一下定义:js引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”

showName()
console.log(myname)
var myname = '极客时间'
function showName() {
    console.log('函数showName被执行');
}

输出:undefined
大家都知道是因为变量提升,但是变量为什么提升呢?
js代码先编译后执行,编译后会生成执行上下文与可执行代码,可执行上下文中包括变量环境与词法环境,在编译时,执行到有函数与var声明的变量,会在变量环境中进行创建,之后js引擎会把声明之外的代码编译为字节码 疑问:

  • function函数在变量环境中怎么存储的
  • 字节码细节是什么
  • 编译与执行阶段具体流程是什么

总结:

  • 如果是同名的函数,JavaScript编译阶段会选择最后声明的那个。
  • 如果变量和函数同名,那么在编译阶段,变量的声明会被忽略。 最后开胃小菜:
var a = 1;
function b(){
    a = 10;
    return;
    function a(){
        console.log(a);
    }	
}
b();
console.log(a);

根据变量提升:

function b() {
    function a() {
        console.log(a);
    }
    a = 10;
    return;
}
var a = undefined
a = 1;
b();
console.log(a)

a=10赋值的时候,会先查找有没有a变量进行赋值,发现找到名为a的函数后,不再向外查找,将10赋值给了函数名为a的函数对象,所以结果为

2.多个执行上下文怎么进行管理

通过栈的数据结构进行管理

var a = 2
function add(b,c){
  return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return  a+result+d
}
addAll(3,6)

上面代码的调用栈: 但是栈是有大小的,栈溢出了怎么办呢?

function runStack (n) {
  if (n === 0) return 100;
  return runStack( n- 2);
}
runStack(50000)

加入定时器处理,改成以下方式:

  if (n === 0) return 100;
  return setTimeout(function(){runStack( n- 2)},0);
}
runStack(50000)

疑问:

  • 为什么改成定时器处理就ok了呢

3.const与let的实现原理

function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a);
    }
}   
foo()
  • 函数作用域内部,通过var声明的被存放在变量环境
  • 被let与const声明的,存放在词法环境 在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出
    输出a,会先沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找 当作用域块执行完毕后,最终的执行上下文如图: 最后开胃小菜:
  • 以下代码输出:
let myname= '极客时间'
{
  console.log(myname) 
  let myname= '极客邦'
}

结果: let不存在变量提升 VM6277:3 Uncaught ReferenceError: Cannot access 'myname' before initialization

  • const,let,var区别 var:
  • 声明的变量在windows
  • 声明的变量可以重复声明
  • 存在变量提升
  • 声明的变量有全局作用域与函数作用域,没有块级作用域的概念 let:
  • 声明的变量在Script
  • 声明的变量不可以重复声明
  • 不存在变量提升
  • 声明的变量有块级作用域的概念
  • const一旦声明必须赋值,声明后不能再修改,如果是复合类型数据,可以修改其属性

4.作用域链与闭包(对作用域与闭包的理解,解释下let与const)

作用域:函数作用域与全局作用域

function bar() {
    console.log(myName)
}
function foo() {
    var myName = "极客邦"
    bar()
}
var myName = "极客时间"
foo()

每个执行上下文中,都包含一个外部引用,用来指向外部的执行上下文,称为outer
查找顺序:会先在当前执行上下文中查找,查找不到就会继续在outer指向的执行上下文中查找,把这个查找的链条称为作用域链
outer指向:作用域链由词法作用域决定,函数声明的位置决定,编译阶段决定,与函数怎么调用没有关系

块级作用域

问题1:

function bar() {
    var myName = "极客世界"
    let test1 = 100
    if (1) {
        let myName = "Chrome浏览器"
        console.log(test)
    }
}
function foo() {
    var myName = "极客邦"
    let test = 2
    {
        let test = 3
        bar()
    }
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()

输出:1
问题2:

var bar = {
    myName:"time.geekbang.com",
    printName: function () {
        console.log(myName)
    }    
}
function foo() {
    let myName = "极客时间"
    return bar.printName
}
let myName = "极客邦"
let _printName = foo()
_printName()
bar.printName()

输出:极客邦

闭包

问题1:对闭包的看法,为什么要用闭包,闭包的原理与应用场景

  • 闭包的定义:根据js词法作用域的规则,内部函数可以访问外部函数的变量,当调用一个外部函数返回了一个内部函数,当该外部函数已经结束,该内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称之为闭包
  • 为什么要用闭包:
    优点:
    1.可以从内部函数访问到外部函数的作用域变量,并且访问到的变量长期驻扎在内存中,供之后使用
    2.避免变量污染全局
    3.把变量存到独立的作用域,作为私有成员的存在 缺点:
    1.对内存消耗与负面影响,因为内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大了内存使用量,所以使用不当会造成内存泄露
    2.对处理速度有负面影响,闭包的层级决定了引用的外部变量在查找时经过的作用域链的长度
    3.可能获取到意外的值
  • 闭包的原理与应用场景
function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = {
        getName:function(){
            console.log(test1)
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

执行到bar.setName("极客邦"),js引擎会按照当前执行上下文-->函数闭包-->全局执行上下文查找 调用栈: 应用场景:

  • 返回一个内部函数
  • 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包
  • 作为函数参数传递的形式
var a = 1;
function foo() {
  var a = 2;
  function baz() {
    console.log(a);
  }
  bar(baz);
}
function bar(fn) {
  // 这就是闭包
  fn();
}
foo(); // 输出2,而不是1
  • 立即执行的函数

5.this

var bar = {
    myName:"time.geekbang.com",
    printName: function () {
        console.log(myName)
    }    
}
bar.printName();

由于bar.printName()调用希望得到的数据是"time.geekbang.com",而不是外部的变量,这也是一个普遍的需求,所以引入了this,不要与作用域链搞混了

var bar = {
    myName:"time.geekbang.com",
    printName: function () {
        console.log(this.myName)
    }    
}
bar.printName();
  • 输出为:time.geekbang.com
  • 结构:
  • this的类型:执行上下文有:全局,函数,eval(执行上下文),所以this也有这三种
  • 容易搞混的点:嵌套函数中的 this 不会从外层函数中继承
var myObj = {
  name : "极客时间", 
  showThis: function(){
    console.log(this)
    function bar(){console.log(this)}
    bar()
  }
}
myObj.showThis()

输出为:window

  • 如何更改this指向:
let bar={
  myName:"郭海平",
  test:1
}
function foo() {
  console.log(this);
  this.myName="极客时间"
}
foo.call(bar);
console.log(bar);

改为箭头函数,或者保存一个变量 疑问:call apply && bind 原理实现
问题1:

var length = 88;
function test() {
  console.log(this.length);
}
let obj = {
  length: 99,
  action: function (test) {
    test();
    arguments[0]();
  },
};
obj.action(test, [1, 2, 3, 4]);

1.没有调用者,默认绑定到全局(window)
2.有调用者:arguments[0]代表数组里面获取第0项,执行此方法,length代表的数组的长度 结果:88 2
问题2:

var a = 10;
function test() {
  a = 100;
  console.log(this.a);
}
test();

var定义的作用与全局变量,a=100实际是更改了全局变量的值
问题3:

var a = 10;
function test() {
  var a = 100;
  console.log(this.a);
}
test();

this指向的是全局变量,所以输出为10
当全局变量与局部变量同名,全局变量不会作用于局部变量
问题4:

var a = 10;
function test() {
  console.log(a);
  a = 100;
  console.log(this.a);
  var a;
  console.log(a);
}
test();

var存在变量提升,代码都是先编译后运行:10,100,100
问题5:

var a = 10;
function f1() {
  var b = 2 * a;
  var a = 20;
  var c = a + 1;
  console.log(b); 
  console.log(c); 
}
f1();

结果:NAN,21