专业编程基础技术教程

网站首页 > 基础教程 正文

前端进阶之路(一):深入理解JavaScript10个概念

ccvgpt 2024-07-29 13:23:45 基础教程 8 ℃


1.js运行环境

js作为脚本语言运行在浏览器中,浏览器就是js的运行环境。对于众多风云的浏览器厂商来说, 他们的内核又是不一样的。浏览器内核分为两种:渲染引擎和js引擎。

前端进阶之路(一):深入理解JavaScript10个概念

渲染引擎:负责网页内容呈现的。

Js引擎:解释js脚本,实现js交互效果的。

1.1常见的内核:

1.2 现在我们有一个js文件,那么浏览器是如何执行它的呢?

首先我们js文件以scirpt标签元素呈现在html里面的。浏览器根据html文件以此解析标签,当解 析到scirpt标签时,会停止html解析,阻塞住,开始下载js文件并且执行它,在执行的过程中,如 果是第一个js文件此时浏览器会触发首次渲染(至于为什么,自己做下实验,不懂的可以留言)。 所以出现一个问题js文件大大阻碍了html页面解析及渲染,所以引入async和defer两个属性(对于 首屏优化有很大的提升,也要谨慎使用)

async:开启另外一个线程下载js文件,下载完成,立马执行。(此时才发生阻塞)

defer:开启另一个线程下载js文件,直到页面加载完成时才执行。(根本不阻塞)

2.js数据类型

基本数据类型:

string:由多个16位Unicode字符组成的字符序列,有单引号或双引号表示

number:采用了IEEE754格式来表示整数和浮点数值

boolean:有两个字面值,true和false.区分大小写的

null:只有一个值的数据类型,值为null.表示一个空对象指针,但用typeof操作会返回一个对象。一般 我们把将来用于保存对象的变量初始化为null.

undefined:这个类型只有一个值,在声明变量未进行赋值时,这个变量的值就是undefined.

Symbol:唯一的值。

引用数据类型:

object:就是一组数据和功能的集合,无序的键值对的方式存储。可以通过new操作符和创建对象构造函数 来创建。常见的对象类型有array,date,function等.

经典面试题:

0.1+0.2为什么不等于0.3?

0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现 了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成 0.30000000000000004。
数据类型检测方式:

1.typeof

typeof检测null是一个对象

typeof检测函数返回时一个function

typeof检测其他对象都返回 object

2.instanceof(下一节手写)

只要在当前实例的原型链上,用instanceof检测出来的结果都是true,所以在类的原型继承中,最后检测 出来的结果未必是正确的.而且instanceof后面必须更一个对象。
不能检测基本类型

3. constructor:

每个构造函数的原型对象都有一个constructor属性,并且指向构造函数本身,由于我们可以手动修改 这个属性,所以结果也不是很准确。 不能检测null和undefined

4.Object.prototype.toString.call(最佳方案)

调用Object原型上的toString()方法,并且通过call改变this指向。返回的是字符串

3.js类型转换

javaScript作为一门弱类型语言,本质为一个变量可以被赋予不同的数据类型。代码简洁灵活,但稍有 不慎,会出现很多坑。

javaScript也作为一门动态类型语言,在运行时,可以随便改变其变量的结构。

所以js变量可以做任意的类型转换,有两种方式,显示类型转换和隐士类型转换。

但是能转换的类型只有三种:to Number,to String,to Boolean.

当基本类型转换成上述类型时会调用:Number() ,String(), Boolean()

只有'' 0 null undefined NaN false转换boolean为false,其他都为true

当引用类型转换时,就稍微有些复杂,我们来举个例子:(所有对象转换boolean都为true)

let obj={
    value:'你好啊',
    num:2,
    toString:function(){
        return this.value
    },
    valueOf:function(){
        return this.num
    },   
}
console.log(obj+'明天')  //2明天
console.log(obj+1)    // 3
console.log(String(obj))   // 你好啊

当对象进行类型转换时:

1.首先调用valueOf,如果执行结果是原始值,返回,如果不是下一步

2.其次调用toString,如果执行结果是原始值,返回,如果不是,报错。

特殊情况:

当使用显示类型转换成String时,执行顺序则是先调用toString,其次调用valueOf

显示类型转换:

Number() / parseFloat() / parseInt()/String() / toString()/Boolean()

隐士类型转换:

+ - == !><= <= >=

3.1经典面试题:(都能答对真的很厉害了,留个名让我关注膜拜一下)

1 + '1' 
true + 0
{}+[]
4 + {} 
4 + [1] 
'a' + + 'b'
console.log ( [] == 0 )
console.log ( ! [] == 0 )
console.log ( [] == ! [] )
console.log ( [] == [] )
console.log({} == !{})
console.log({} == {})
一错就知道,一做又全忘哈哈哈哈。
答案:
'11'
1
0
"4[object Object]"
"41"
"aNaN"
true
true
true
false
false
false

4.js遍历

4.1对象遍历:

1.for in:自身和继承属性,可枚举,不含Symbol

2.Object.keys(obj):可枚举,不含Symbol,自身

3.Object.values(obj):可枚举,不含Symbol,自身

4.Object.getOwnPropertyNames(obj):自身所有属性,不含Symbol

5.Reflect.ownKeys(obj):自身所有属性

4.2 数组遍历:

forEach,map,filter,every,some,reduce等.

4.3 字符串遍历:

for in

4.4 Set数据结构:

Set.prototype.keys():返回键名的遍历器

Set.prototype.values():返回键值的遍历器

Set.prototype.entries():返回键值对的遍历器

Set.prototype.forEach():回调函数遍历每个成员

4.5 Map数据结构:

Map.prototype.keys():返回键名的遍历器

Map.prototype.values():返回键值的遍历器

Map.prototype.entries():返回键值对的遍历器

Map.prototype.forEach():回调函数遍历每个成员

5.作用于与作用域链

5.1作用域

javascript采用的静态作用域,也可以称为词法作用域,意思是说作用域是在定义的时候就创建了, 而不是运行的时候。此话对于初学者很不好理解,看看下面这个例子:

let a=1
function aa(){
    console.log(a)    //输出1
}
function bb(){
    let a=2
    aa()
}

是不是非常违背常理啊,你看嘛,aa在bb里面调用的,aa函数里面没有a变量,那么就应该去调用它的作 用域里找,刚好找到a等于2。思路是完美的,可是js的作者采用的静态作用域,不管你们怎么运行,你们 定义的时候作用域已经生成了。

那么什么是作用域?

变量和函数能被有效访问的区域或者集合。作用域决定了代码块之间的资源可访问性。

作用域也就是一个独立的空间,用于保护变量防止泄露,也起到隔离作用。每个作用域里的变量可以相同命名,互不干涉。就像一栋房子一样,每家每户都是独立的,就是作用域。

作用域又分为全局作用域和函数作用域,块级作用域。 全局作用于任何地方都可以访问到,如window,Math等全局对象。 函数作用域就是函数内部的变量和方法,函数外部是无法访问到的。 块级作用域指变量声明的代码段外是不可访问的,如let,const.

5.2作用域链

知道作用域后,我们来说说什么是作用域链?

表示一个作用域可以访问到变量的一个集合。函数作为一个对象有一个[[scope]]属性,就是表示这个集合的。再来理解几个概念词:

AO:活动变量(Active object,VO)

VO:变量对象(Variable object,VO)

执行上下文:代码运行的环境,分为全局上下文和函数上下文。

举例子来说明一下:(借用的例子)
// 作者:jianyangdu洋仔
function a() {
        function b() {
            var b = 234;
        }
        var a = 123;
        b();
    }
    var gloab = 100;
    a();

第一步: a 函数定义


我们可以从上图中看到,a 函数在被定义时,a函数对象的属性[[scope]]作用域指向他的作用域链scope chain,此时它的作用域链的第一项指向了GO(Global Object)全局对象,我们看到全局对象上此时有5个属性,分别是this、window、document、a、glob。

第二步: a 函数执行


当a函数被执行时,此时a函数对象的作用域[[scope]]的作用域链scope chain的第一项指向了AO(Activation Object)活动对象,AO对象里有4个属性,分别是this、arguments、a、b。第二项指向了GO(Global Object),GO对象里依然有5个属性,分别是this、window、document、a、golb。

第三步: b 函数定义


! 当b函数被定义时,此时b函数对象的作用域[[scope]]的作用域链scope chain的第一项指向了AO(Activation Object)活动对象,AO对象里有4个属性,分别是this、arguments、a、b。第二项指向了GO(Global Object),GO对象里依然有5个属性,分别是this、window、document、a、golb。


第四步: b 函数执行

当b函数被执行时,此时b函数对象的作用域[[scope]]的作用域链scope chain的第一项指向了AO(Activation Object)活动对象,AO对象里有3个属性,分别是this、arguments、b。第一项指向了AO(Activation Object)活动对象,AO对象里有4个属性,分别是this、arguments、a、b。第二项指向了GO(Global Object),GO对象里依然有5个属性,分别是this、window、document、a、golb。 以上就是上面代码执行完之后的结果。


6.闭包

引自:https://github.com/mqyqingfeng/Blog/issues/9

不会闭包的程序员不是好程序员。

闭包的官方定义:

mdn:闭包是指那些能够访问自由变量的函数。

维基百科:在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性.

我:一个作用域可以访问另一个作用域的变量,就产生闭包。之前比喻作用域就好比一栋房子每一户,闭包相当于串门。

什么是自由变量?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

闭包=函数+函数能够访问的自由变量。

从技术的角度讲,所有的JavaScript函数都是闭包。

var a = 1;

function foo() {
    console.log(a);
}

foo()

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。

那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……

我们在看看ECMAScript中,闭包指的是:

1.从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

2.从实践角度:以下函数才算是闭包:

即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)

在代码中引用了自由变量

下面我们开始实践论证:(例子依然是来自《JavaScript权威指南》)

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
var foo = checkscope();
foo();
输出 "local scope"

因为变量查找的规则是通过作用域链的,作用域链是在函数定义的时候就已经确定了, 所以我们来看看定义f函数时候的[[scope]]属性:

[
    AO:{
        scope:"local scope",
        f:function
    },
    global:{
     scope :"local scope",
     checkscope:function
    }
]

f执行时候的[[scope]]属性:

[
    AO:{
        arguments:[],
        this:window
    },
    AO:{
        scope:"local scope",
        f:function
    },
    global:{
     scope :"local scope",
     checkscope:function
    }
]
根据先后顺序scope变量输出为"local scope"

经典面试题:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}
data[0]();
data[1]();
data[2]();
答案是都是 3,让我们分析一下原因:

当执行到 data[0] 函数之前,此时全局上下文的 VO 问:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3

7.原型及原型对象

不多说,要会背下面这张图,理解起来很简单的。

先谈一下我的理解吧:

javascript万物皆对象,每个对象都有一个__proto__属性,指向了创造它的构造函数的原型对象。

每个函数都有一个原型对象,prototype,当使用new 创造对象时继承这个对象。

function A(){}
var a=new A()
a.__proto__===A.prototype

下面就有问题了,谁创造了A这个构造函数呢,还有谁创造了A.prototype这个对象呢?

这时候我们就要知道js两个顶级函数,Function,Object

所有函数都是由Function创建的

A.__proto__===Function.prototype

刚说了所有函数都是由Function创建的,也包括自己。也就是说Function创造了自己:

Function.__proto__===Function.prototype

Object刚讲的是顶级函数,所以也是函数:(所有的鱼都归猫管哈哈哈哈哈)

Object.__proto__===Function.prototype

所有的对象都是由Object构造函数创建的:

A.prototype.__proto__===Object.prototype

那么Object.prototype也是对象啊,是由谁创建的呢,记住万物皆空,何尝不是人生,到头来什么都会没有。

Object.prototype.__proto__===null

原型链(一种访问机制):

1.在访问对象的某个成员的时候会先在对象中找是否存在

2.如果当前对象中没有就在构造函数的原型对象中找

3.如果原型对象中没有找到就到原型对象的原型上找

4.直到Object的原型对象的原型是null为止

8.this指向问题

这个问题直接从结果入手,this指向一共有七种情况,下面一 一说起。

8.1 全局环境 普通函数调用,普通对象

const obj={a:this}
obj.this===window  //true
function fn(){
    console.log(this)   //window
}

8.2 构造函数

  function a() {
    console.log(this)
  }
  const obj = new a()   //  a{}
  a()                    // 'window'

new出来的对象,this指向了即将new出来的对象。
当做普通函数执行,this指向window。

8.3 对象方法

  const obj = {
    x: 0,
    foo: function () {
      console.log(this)
    }
  }
  obj.foo()                 // obj
  const a = obj.foo
  a()                       //window

作为对象方法,this指向了这个对象。(新对象绑定到函数调用的this)
一旦有变量直接指向了这个方法,this为window.

特殊情况

如果在方法里面执行函数,this指向window.

  const obj = {
    x: 0,
    foo: function () {
      console.log(this)      // obj
      function foo1() {
        console.log(this)    //window
      }
      foo1()
    }
  }
  obj.foo()   

8.4 构造函数prototype属性

  function Fn() {
    this.a = 10
    let a = 100
  }
  Fn.prototype.fn = function () {
    console.log(this.a)             // 10 说明指向了obj这个对象
  }
  const obj = new Fn()
  obj.fn()

原型定义方法的this指向了实例对象。毕竟是通过对象调用的。

8.5 call ,apply, bind

  const obj = {
    x: 10
  }
  function fn() {
    console.log(this)
  }
  fn.call(obj)      //obj
  fn.apply(obj) //obj
  fn.bind(obj)() //obj

this指向传入的对象。

8.6 DOM事件

 document.getElementById('app').addEventListener('click', function () {
    console.log(this)           // id为app的这个对象
  })

指向绑定事件的对象。

8.7 箭头函数

  obj = {
    a: 10,
    c: function () {
      b = () => {
        console.log(this)           //指向obj
      }
      b()
    }
  }
  obj.c()

在方法中定义函数应该是指向window,但是箭头函数没有自己的this,所以指向上一层作用域中的this.

 document.getElementById('app').addEventListener('click', () => {
    console.log(this)           // 改为箭头函数,指向了window,而不是触发对象
  })

8.8 绑定方式:

隐士绑定:

谁调用方法,this指向谁。

显示绑定

call,bind,apply

new 绑定

优先级问题:

new>显示绑定>隐式绑定

经典面试题:

上面知道了没啥难的。

9.继承

原型链继承

构造函数继承

组合继承

寄生组合继承

extends继承

9.1 原型链继承:

 function Animal() {
    this.name = 'cat'
    this.msg = {
      age: 9
    }
  }
  Animal.prototype.greet = function () {
    console.log('hehe')
  }
  function Dog() {
    this.name = 'dog'
  }
  Dog.prototype = new Animal()  //核心一步

  const a = new Dog()
  a.msg.age = '99'
  const b = new Animal()

缺点:多个实例对应用类型操作会被篡改

9.2 构造函数继承:

function Animal() {
    this.name = 'cat'
    this.msg = {
      age: 9
    }
  }
  Animal.prototype.greet = function () {
    console.log('hehe')
  }
  function Dog() {
   Animal.call(this)            // 核心一步
  }
const a=new Dog()

缺点:

只能继承父类的实例属性和方法,不能继承原型属性/方法。

性能不好,每个子类都会拥有父类实例的副本。

9.3 组合继承:

就是将上两种方法结合起来

function Animal() {
    this.name = 'cat'
    this.msg = {
      age: 9
    }
  }
  Animal.prototype.greet = function () {
    console.log('hehe')
  }
  function Dog() {
   Animal.call(this)            // 核心一步
  }
  Dog.prototype = new Animal()  // 核心一步
const a=new Dog()

9.4原型式继承

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

不能做到函数复用

共享引用类型属性的值

无法传递参数 缺点:

function inheritObject(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

var situation = {
    companies:['bigo','yy','uc'];
    area:'guangzhou';
}

var situationA = inheritObject(situation);
console.log(situationA.area)     //'guangzhou'

9.5 寄生式继承

在原型式继承的基础上,增强对象,返回构造函数.

缺点同上

 function createAnother(original){
  var clone = object(original); // 或 Object.create(original) 
  clone.sayHi = function(){  // 以某种方式来增强对象
    alert("hi");
  };
  return clone; // 返回这个对象
}
var person = {
  name: 'Nicholas',
  friends : ["Shelby","Coury","Van"]
}

var anotherPerson  = createAnother(person) 

9.6 extends(es6)

借用阮一峰老师的es6中extends继承,眼过千遍,不如手写一遍。

上面的只能说去应付面试,这个才是我们开发中最常用的,所以必须掌握。

写法:
class Point {
}

class ColorPoint extends Point {
}

上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法

Object.getPrototypeOf可以使用这个方法判断,一个类是否继承了另一个类
Object.getPrototypeOf(ColorPoint) === Point    //true

Super关键字

class A {}

class B extends A {
  constructor() {
    super();
  }
}

上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。

注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

上面代码中,new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B。

作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

class A {}

class B extends A {
  m() {
    super(); // 报错
  }
}

上面代码中,super()用在B类的m方法之中,就会造成语法错误。

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

上面代码中,p是父类A实例的属性,super.p就引用不到它。

如果属性定义在父类的原型对象上,super就可以取到。

class A {}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}

let b = new B();

上面代码中,属性x是定义在A.prototype上面的,所以super.x可以取到它的值。

ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2

上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)。

由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

let b = new B();

上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。

如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。

class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

10.数据存储与传参

在JavaScript中,每一个变量在内存中都需要一个空间来存储。内存空间又分为栈内存和堆内存。 基本数据类型保存在栈中,引用类型保存于堆中。

let a='a'
a='b'
let b=a

首先创建变量a,再创建字符串'a',使变量a指向'a',a保存的是这个值

接着又创建字符串'b',使变量a指向字符串'b',同时删除'a'

深拷贝了一份a(重新创建了'b'),使变量b指向了'b',变量a和变量b互不受影响。

let obj1=new Object()
let obj2=obj1
obj1.name='node'
alert(obj2.name)           //node

创建一个对象,开辟了一个堆内存,使变量obj1指向这个对象(堆内存)的地址

创建一个变量obj2,将obj1的值赋值给obj2,就是将地址赋值给了obj2,这时obj1和obj2指向的是同一个堆内存两个变量就相互影响。

传递参数

所有函数的参数都是按值传递的。也就是说,函数外部的值。复制给函数内部的参数.

function add(num){
    num+=10
    return
}
var counrt=20
var result=add(counrt)
alert(counrt)        //20,没有变化
alert(result)       //30
function setName(obj){
  obj.name='node'
}
var person=new Object()
setName(person)

alert(person.name)       //'node'
function setName(obj){
  obj.name='node'
  obj={name:'java'}
}
var person=new Object()
setName(person)

alert(person.name)  //'node'

分析:之前说过,函数参数是按值传递的。所以在函数内部,obj这个局部变量保存的是person这个对象的地址。第一步操作了这个地址下的name属性为'node',第二步,就是将这个变量指向了一个新的堆地址,所以外部person对象丝毫不受影响,name属性依旧为node.

Tags:

最近发表
标签列表