JS基础知识

116 阅读10分钟

1. 基础知识

关于脚本加载、流程控制语句、操作符、变量声明相关基础概念汇总在以下图片中:

1.1 脚本加载

1.2 流程控制语句

1.3 操作符

1.4 变量声明

2. 面向对象的程序设计

提到面向对象,大家的第一反应就是封装、继承和多态。

封装: 隐藏细节(A对A—将多行代码取个名字;或A对B—API调用合作) 继承: 继承的意思就是同上跟上述一样,直接用另外一件事情的属性和方法,只需要写自己需要的属性和方法即可。 多态: 一个东西拥有两种或者多种东西的属性

因此,封装使我们减少沟通成本,也减少思维负担;继承可以让我们复用代码;而多态可以使一个东西更加灵活。

2.1 封装

实现对象的封装,分为以下三个步骤:

  • 创建类

    首先声明一个函数保存在一个变量中(类名首字母大写),然后将这个函数(类)的内部通过对this对象添加属性或者方法来实现对类进行属性或方法的添加,例如:

    var Person = function (name, age ) {
        this.name = name;
        this.age = age;
    }
    

    除了使用this,还可以通过类的原型对象(prototype)给类添加属性和方法,有两种方式:

    1)为类的原型对象的属性赋值

    Person.prototype.showInfo = function () {
    	console.log('My name is ' + this.name);
      }
    

    2)将一个对象赋值给类的原型对象

    Person.prototype = {
      showInfo : function () {
          console.log('My name is ' + this.name );
      	}
      }
    

    以上两种方式的不同: 我们通过this定义的属性或者方法都是该对象自身拥有的,我们每次通过new运算符创建一个新对象时,this指向的属性和方法也会得到相应的创建,但是通过prototype继承的属性和方法是每个对象通过prototype访问得到,每次创建新对象时这些属性和方法是不会被再次创建的,如下图所示: 其中constructor是一个属性,当创建一个函数或者对象的时候都会给原型对象创建一个constructor属性,指向拥有整个原型对象的函数或者对象。

    注意: 采用第二种方式是将一整个对象赋值给了原型对象,这样会导致原来的原型对象上的属性和方法会被全部覆盖掉,那么constructor的指向当然也发生了变化,这就导致了原型链的错乱,因此,我们需要手动修正这个问题,在原型对象上手动添加上constructor属性,重新指向Person,保证原型链的正确,即:

   Person.prototype = {
		constructor : Person ,
		showInfo : function () {
			console.log('My name is ' + this.name);
		}
    }
  • 封装属性与方法

    类相关的属性和方法如下所示:包括私有属性、私有方法、特权方法、实例属性、实例方法、类静态属性、类静态方法、公有属性、公有方法。

    特权方法: 在类的外部被调用,它既可以访问类的私有属性,也是可以访问类的公有属性,可以勉强的认为它是一种特殊的公有方法。特权方法必须在类的内部声明定义。利用的闭包原理,即通过作用域链,让内部函数能够访问外部函数的变量对象(即该类的私有变量、私有方法)。个人理解:特权方法与实例方法的区别在于,实例方法(类专属方法)必须通过实例对象访问,而特权方法(函数<类也是函数>专属方法)除了可以通过实例访问还可以通过一下方式访问。例如:

    var person=function(){
            var name="Bob";    //私有变量
            function privateFunction(){    //私有函数
                alert(true);    
            };
            return {
                publicName:name,    //特权属性
                publicMethod:function(){    //特权方法
                    return privateFunction();    
                }                
            }        
        }();
      
      alert(person.publicName);    //Bob
      person.publicMethod();    //true
    

    类静态属性和类静态方法: 通过new创建的对象无法通过 . 访问(即,不会被实例继承),只能通过类本身来访问。

    var Person = function (name, age ) {
    	    //私有属性
    	    var IDNumber = '01010101010101010101' ;
    	    //私有方法
            function checkIDNumber () {}
            //特权方法
            this.getIDNumber = function () {}
            //实例属性
            this.name = name;
            this.age = age;
            //实例方法
            this.getName = function () {}
	}
    	//类静态属性
        Person.isChinese = true;
        //类静态方法
        Person.staticMethod = function () {
            console.log('this is a staticMethod')
        }
        //公有属性
        Person.prototype.isRich = false;
        //公有方法
        Person.prototype.showInfo = function () {}
  • 创建对象

    非安全模式:

    //创建一个类
    var Person = function (name, age ) {
    	this.name = name;
    	this.age = age;
    }
    var person = Person('Tom',24)
    
    console.log(person)  // undifined
    console.log(window.name)  // Tom
    console.log(window.age)   // 24
    

    使用非安全模式创建类,当我们没有使用new操作符来创建对象时,执行Person方法就是在全局作用域中执行,此时this指向的也就是全局变量,也就是window对象,所以添加的属性都会被添加到window上,而我们的person变量在得到Person的执行结果时,由于函数中没有return语句,默认返回了undifined。

    安全模式:

     var Person = function (name, age) {
     	// 判断执行过程中的 this 是否是当前这个对象 (如果为真,则表示是通过 new 创建的)
     	if ( this instanceof Person ) {
     		this.name = name;
     		this.age = age;
     	} else {
     		// 否则重新创建对象
     		return new Person(name, age)
     	}
     }
    

2.2 继承

继承是为了子类可以使用父类的所有功能,并且能对这些功能进行扩展。实现继承有以下五种方式:

1)构造函数继承(call&apply)

说明:直接利用call或者apply方法将父类构造函数的this绑定为子类构造函数的this就可以;
缺点:无法继承原型链上的属性与方法;

2)原型继承

说明:将子类的原型挂载到父类上;
缺点:子类new出来的实例,父类的属性没有隔离,会相互影响;

3)组合继承

说明:组合上面的构造函数与原型继承的功能;
缺点:call()方法已经拿到父类所有的属性 ,后面再使用原型时也会有父类所有属性;

4)寄生组合继承

说明:解决组合继承重复属性的问题,直接将子类的原型等于父类的原型,或者是用Object.create继承原型但不执行父类构造函数;
注意:记得处理子类实例的constructor指向问题;

5) Class继承

说明:ES6新增,class是一个语法糖,就是基于寄生组合继承来实现的;
class Parent5{
      constructor(){
         this.name = 'Parent5'
         this.arr = [1, 2, 3]
      }
      say(){
         console.log(this.name)
      }
}
 class Child5 extends Parent5{
       constructor(){
           super() //通过super()调用父类构造函数
           this.type="Child5"
         }
 }
 let child5_1 = new Child5()
 let child5_2 = new Child5()
 child5_1.arr.push(4)
 console.log(child5_1.say()) // Parent5
 console.log(child5_1.arr, child5_2.arr) // [1, 2, 3, 4]  [1, 2, 3]
 console.log(child5_1.constructor === Child5) // true 
 console.log(child5_2.constructor === Parent5) // false

注意: constructor其实没有什么用处,只是JavaScript语言设计的历史遗留物。由于constructor属性是可以变更的,所以未必真的指向对象的构造函数,只是一个提示。不过,从编程习惯上,我们应该尽量让对象的constructor指向其构造函数,以维持这个惯例。

2.3 多态

定义:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果,也就是说,给不同的对象发送同一个消息时,这些对象会根据这个消息分别给出不同的反馈。

作用:通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句

3. 函数表达式

3.1 递归

定义:通俗的来说,就是通过不断的将当前问题进行分解,向前追溯直到终点然后再反推求解的过程。

表现形式:递归函数是指一个函数在自身内部调用本身。

满足条件:1) 可以分解为子问题;2) 这个问题与分解之后的子问题,除了数据规模不同,其他都是相同的;3) 有终止条件;

实例1:实现n的阶乘

function fn1(a) {
  if (a === 1) return 1;
  return a * fn1(a-1);
}
console.log(fn1(5));

实例2:走格子

一共有n格,每步可以走1格或者2格,问一共有多少走法。 首先分解问题是第n格可以是前面n-1格走的,也可能是n-2格走的。所以fn(n) = f(n-1) + f(n-2)。也要知道终止条件是只有1步,那么只有一步的可能情况是还有1格,也可能是还有2格。

function fn = (n){
  if(n>2){
  	return fn(n-1) + fn(n-2)
  } else if(n==2)   {
  	return 2
  } else {
  	return 1
  }
}

问题:

1、当递归层级过深的时候,因为在递归的过程中会一直把临时变量封装为栈压入内存栈,如果一直压入,就会导致溢出导致服务崩溃。(可以通过变量控制递归的深度,但这种不是很实用,因为内存一般是动态变化的,用定值没意义,而如果动态获取内存,又小题大做了。)

2、重复计算,如下图示。在得到最终结果前,对于某些节点进行了重复计算,严重浪费了计算资源。 优化方法:针对已经得到结果的走法计到Map缓存中直接使用。

let  f  = ( n) => {
  if (n == 1) return 1;
  if (n == 2) return 2;
  // hasSolvedList可以理解成一个Map,key是n,value是f(n)
  if (hasSolvedList.has(n)) {
    return hasSovledList.get(n);
  }
  ley ret = f(n-1) + f(n-2);
  hasSovledList.add(n, ret);
  return ret;
}

3.2 闭包

定义:闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。(就算创建一个全局的函数,闭包也会被同时创建,因为该全局函数的内部作用域可以访问外部的全局变量,例如下面代码:

    var a = 1;
    var b = 1;
    function add () {
    	console.log(a + b);
    }

误区: 并不是一定要return内部函数,即外部函数销毁后其活动对象由于被内部函数引用而没有被销毁,才形成闭包。闭包是一种结构!!!例如下面代码:

    function wrap () {
        var a = 1;
        var b = 1;
    	function add() {
        	console.log(a + b);
        };
    }

特性: 一个函数可以访问另一个函数的变量;当内函数使用了外函数的局部变量时,外函数的局部变量与内函数发生绑定,从而延长该变量的生命周期(该特性与前面误区表达的内容一致)。利用这两特性,可以实现js中一些高级功能。

实例1:让外部可以访问内部变量

    function test(){
        var a = 10;
        return function(){
            console.log(a);
        }
 	}
     let testFn = test();
     testFn();

实例2:缓存变量(与js高级程序设计第三版P181,example01.htm类似)

    const once = (fn)=>{
    let done = false;
      return function(){
          if(!done){
              fn.apply(this,fn);
          }else{
              console.log("this fn is already execute");
          }
          done = true;
      }
  }

    function test(){
        console.log("test...");
    }
    let myfn =  once(test);
    myfn();  // test...
    myfn();  // this fn is already execute

4. BOM(浏览器对象模型)

4.1 window 对象

4.2 location 对象

4.3 navigator 对象

4.4 screen 对象

4.5 history 对象

5. 客户端检测

检测Web客户端的手段很多,而且各有利弊,但是不到万不得已,不要使用客户端检测。只要能找到更通用的方法,就应该采用更通用的方法。先设计最通用的方案,然后再使用特定于浏览器的技术增强该方案。

5.1 能力检测

能力检测的目标不是识别特定的浏览器,而是识别浏览器的能力。

5.2 怪癖检测

怪癖检测的目的是识别浏览器的特殊行为。

5.3 用户代理检测

6. DOM(节点对象模型)

7. 事件

7.1 事件流

7.2 事件处理程序

7.3 事件对象

7.4 事件类型

7.5 内存、性能;模拟事件

8. 错误处理与调试

8.1 浏览器报告的错误

8.2 错误处理

8.3 调试技术

8.4 常见的IE错误

9. Ajax与Comet

10. 高级技巧

10.1 高级函数

10.2 防篡改对象

10.3 高级定时器

10.4 自定义事件

10.5 拖放

11. 离线应用与客户端存储

11.1 离线检测

11.2 应用缓存

11.3 数据存储

12. 最佳实践

12.1 可维护性

12.2 性能

12.3 部署

13. 新兴的API

拓展:TypeScript 面向对象程序设计

在 JavaScript 中 ES6 之前我们使用函数(构造器函数)和基于原型来创建一个自定义的类,但这种方式总会让人产生困惑,特别是习惯了 Java、PHP 等面向对象编程的同学来说更加难以理解。