前端之路(JS高级篇)

面向对象编程

什么是对象??

(1) 从视觉角度 : 对象是单个事物的抽象。

一本书、一个人都可以是对象,一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

(2) 从编程角度 : 对象是无序键值对的集合,其属性可以包含基本值、对象或者函数等等

每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型。

 

什么面向对象 ?

面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。

它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
​
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
​
因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。

员工才面向过程,老大都是面向对象

面向对象与面向过程:

  • 面向过程就是亲历亲为,事无巨细,面面俱到,步步紧跟,有条不紊,面向过程是解决问题的一种思维方式,关注点在于解决问题的过程。

  • 面向对象就是找一个对象,指挥得结果,解决问题的思维方式,关注点在解决问题的对象上。

  • 面向对象将执行者转变成指挥者

  • 面向对象不是面向过程的替代,而是面向过程的封装

面向对象的特性:
​
- 封装性
  - 将功能的具体实现,全部封装到对象的内部,外界使用对象时,只需要关注对象提供的方法如何使用,而不需要关心对象对象的内部具体实现,这就是封装。
- 继承性
  - 在js中,继承的概念很简单,一个对象没有的一些属性和方法,另外一个对象有,
  - 注意:在其他语言里面,继承是类与类之间的关系,在js中,是对象与对象之间的关系。
- [多态性]
  - 多态是在强类型的语言中才有的。js是弱类型语言,所以JS不支持多态。

创建对象的方式:

单独创建 ( 2种 ) 和 批量创建 ( 2种 )

字面量创建对象

缺点 : 不能很方便的批量创建

var person = {
  name: 'Jack',
  age: 18,
  sayName: function () {
    console.log(this.name)
  }
}

构造函数 Object 创建对象

缺点 : 属性要一个一个的添加,也是 不能很方便的批量创建,

//在js中,对象有动态特性,可以随时的给一个对象增加属性或者删除属性。
var person = new Object()
person.name = 'Jack'
person.age = 18
​
person.sayName = function () {
  console.log(this.name)
}

工厂函数 创建对象

缺点 : 但却没有解决对象识别的问题,创建出来的对象都是Object类型的。

// 工厂函数
function createPerson (name, age) {
  return {
    name: name,
    age: age,
    sayName: function () {
      console.log(this.name)
    }
  }
}
// 实例对象
var p1 = createPerson('Jack', 18)
var p2 = createPerson('Mike', 18)

自定义构造函数创建

function Person (name, age) {
  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }
}
​
var p1 = new Person('Jack', 18)
p1.sayName() // => Jack
​
var p2 = new Person('Mike', 23)
p2.sayName() // => Mike

注意点:

1. 构造函数 => 函数 + 首字母大写
2. 构造函数要配合new操作赋一起使用才有意义
3. new的作用 :   (牢记)
    - 创建一个新对象
    - this指向了这个新对象
    - 执行构造函数, 给对象添加属性和方法
    - 返回新对象
4. 构造函数的作用 : 实例化对象 即:给对象赋值,添加属性和方法

构造函数的缺点 :

使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:

function Person (name, age) {
  this.name = name
  this.age = age
  this.type = 'human'
  this.sayHello = function () {
    console.log('hello ' + this.name)
  }
}
​
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)
console.log(p1.sayHello === p2.sayHello) // => false

解决方案 :

// 全局声明的
function sayHello () {
  console.log('hello ' + this.name)
}
​
function Person (name, age) {
  this.name = name
  this.age = age
  this.type = 'human'
    // 先有的sayHello,  你再去指引的
  this.sayHello = sayHello
}
​
​
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)
​
console.log(p1.sayHello === p2.sayHello) // => true

缺点:会暴漏很多的函数,容易造成全局变量污染。

 

原型

原型的基本概念

Javascript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数( Person() )的实例 ( p、p1等 )继承 。 这个对象就是原型,也叫原型对象。

这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。

原型的作用 : 共享数据 (也解决了构造函数资源浪费的问题)

// 原型:
//1. 在js中, 任何一个函数,都会自带一个属性 prototype, 这个属性指向了一个对象
//2. 这个prototype属性,也就是这个对象,我们叫做原型 ; (原型对象)
// 这个原型属性是一个对象
//3. 通过构造函数创建的对象,可以直接访问这个构造函数的原型中的所有内容(属性和方法)
//4. 最常用的就是 : 给构造函数的原型添加一个方法

代码 :

function Person (name, age) {
  this.name = name
  this.age = age
}
​
// dir 或者  log
console.log(Person.prototype)
​
Person.prototype.type = 'human'
​
Person.prototype.sayName = function () {
  console.log(this.name)
}
​
var p1 = new Person(...)
var p2 = new Person(...)
​
console.log(p1.sayName === p2.sayName) // => true  解决了内容浪费的问题

这时所有实例的 type 属性和 sayName() 方法,其实都是同一个内存地址,指向 prototype 对象,因此就提高了运行效率。

 

构造函数、实例、原型三者之间的关系 (★★★)

构造函数 (Person):构造函数就是一个函数,配合new可以新建对象。

实例 (p1, p2):通过构造函数实例化出来的对象我们把它叫做构造函数的实例。一个构造函数可以有很多实例。

原型 (Person.prototype) :每一个构造函数都有一个属性prototype,这个属性就叫做原型对象。通过构造函数创建出来的实例能够直接使用原型上的属性和方法。

 

构造+实例p,打印Person,有prototype属性,画图分配空间,newp,访问原型 p1亦是

注意:方法是给原型加,不是给构造函数加,方法是原型里的方法

继承关系:

实例对象p, 从Person.prototype里访问属性和方法,,这就叫继承 ; (拿过来用)

也就是说 :以后p想要找某个属性或者某个方法,,如果自己没有,,试着去看下原型里是否有

思考:内置对象中,有很多的方法,这些方法存在哪里?

var arr = [] , arr.push() Array.prototype;

var str = 'abc' str.indexOf() => String.prototype

__proto__ (★)

实例对象p和原型对象的关系呢?

通过构造函数创建的对象,自带一个__proro__属性,这个属性指向了构造函数的prototype属性,也就是原型对象。

获取原型对象:

  • 通过构造函数.prototype可以获取

  • 通过实例.__proto__可以获取(隐式原型)

  • 它们指向了同一个对象构造函数.prototype === 实例.__proto__

  • 又因为是浅白色,,这种是私有属性,,不可遍历的,,,,,不要用它来添加属性和方法,,只负责来检测它的原型即可

  console.log(p.__proto__ === Person.prototype);

关系确立

1. 构造函数 是 妈妈,,妈妈只负责生孩子,,创建对象p
2. 原型对象是 爸爸,,,,实例对象能够继承原型对象的属性和方法

constructor属性 (☆)

默认情况下,原型对象中值包含了一个属性:constructor,constructor属性指向了当前的构造函数。

 

原型链

属性查找原则

构造函数实例化的对象 p , 能够访问原型对象里的全部属性和方法

  • 如果是获取操作

  • 沿着 __proto__ 一直往上找

    1. 会先在自身上查找,如果没有
    2. 则根据__proto__对应的原型去找,如果没有
    3. 一直找到Object.prototype,如果没有,那就找不到了。
  • 如果是修改操作

    //1. 只会修改对象自身的属性,
    //2. 如果自身没有这个属性,那么就会添加这个属性,并不会修改原型中的属性。

原型链概念

任何一个对象,都有原型对象,原型对象本身又是一个对象**,所以原型对象也有自己的原型对象,这样一环扣一环就形成了一个链式结构,我们把这个链式结构称为原型链。

__ proto __

//1. var s = new Strudent();
//2. var o = new Object();
//3. var arr = new Array();;
//4. var date = new Date();
//5. Math

总结:Object.prototype是原型链的尽头,Object.prototype的原型是null。

画图的本质 :

1. 通过打印 : p.__proto__ (原型对象)  来找
2. 找到  p.__proto__ (原型对象里的) constructor 的属性
3. 找到构造函数 其他的就解决了
​
​
  //var date = new Date();
  //  date ==> date.__proto__(Date.prototype) ==> Object.prototype ==> null

Object.prototype成员介绍

浅色是不可遍历的

constructor: 返回对创建此对象的构造函数

hasOwnProperty:

isPrototypeOf:

propertyIsEnumerable:

toLocaleString:

toString:

valueOf:

hasOwnProperty (★★)

hasOwnProperty() 方法会返回一个布尔值,判断某个属性是否是对象自己的属性。

// 参数  : 属性字符串
// 返回值 :true或者false
  • 如果 是 自己的属性,会返回true。

  • 如果 不是 自己的属性,会返回false。( 原型链上的属性也不算自己的 ),

function Person(name, age) {
    this.name = name;
    this.age = age;
}
​
Person.prototype.gender = "男";
​
var p = new Person("zs", 18);
console.log(p.hasOwnProperty("name"));
console.log(p.hasOwnProperty("toString"));
console.log(p.hasOwnProperty("hasOwnProperty"));
console.log(p.hasOwnProperty("sex"));
console.log(p.hasOwnProperty("age"));
console.log(p.hasOwnProperty("gender"));

 

拓展 :in(★★)

思考:hasOwnProperty 与 in 的区别? 以及两者的配合使用

  • in 操作符:如果属性 是自己的 + 原型上继承来的,==> true

    // in 能判断的属性
    //1. 自己的属性 OK
    //2. 原型链上的属性也是
    //1. in 的第一个用法 
    if ( 'name' in p )  {
        // 'name' 是p自己的原型上的
    }else {
        // 不是自己的也不是原型上的
    }
    ​
    //2. in 的第二个用法   p.keys
       for (var key in p) {
          console.log(key); // 遍历自己的+原型加的
        }

  • hasOwnProperty: 该属性必须是自己的属性,== > true,否则返回false。

// 在继承属性的情况下 , 打印自己的属性
function Person(name, age) {
    this.nage = name;
    this.age = age;
}
Person.prototype.gender = "男";
​
var p = new Person("zs", 20);
  • 需求练习 : 通过遍历,查找自己的属性

  • for(var k in p) {
        if(p.hasOwnProperty(k)) {
            console.log(p[k]);
        }
    }
    

propertyIsEnumerable  可枚举/可遍历

为什么要学习这个? 因为有些是不可枚举的,什么时候呢??自己添加的时候

propertyIsEnumerable() 方法返回一个布尔值,。 判断该属性是否是当前对象可枚举自身属性

  • 可枚举 ( 可以 for in 的 => 自身属性 + 原型链新加的属性)

  • 自身属性

function Person(name, age) {
    this.name = name;
    this.age = age;
}
​
Person.prototype.gender = "男";
var p = new Person("zs", 19);
​
    for (var key in p) {
      console.log(key); // 可枚举 name age gender
    }
console.log(p.propertyIsEnumerable("name")) //true
console.log(p.propertyIsEnumerable("age")) //true
console.log(p.propertyIsEnumerable("gender")) //false

Object.defineProperty() - 定义一个新属性 (★)

以前的 : p.gender = '女'; 这个是默认的

Object.defineProperty() 这个是更新详细的,细化的步骤

  1. defineProperty的基本上使用

  2. 解决一些问题

//第一个参数:需要给哪个对象添加属性
//第二个参数:给对象添加的属性的名字
//第三个参数:给属性添加的修饰,是一个对象
var obj = {
  name: "age",
  age: 18}
   // 底层 defineProperty 定义或者修改一个属性
      Object.defineProperty(obj , 'sex', {
        value: '女',
        enumerable : true, //可遍历
        writable : true,  // 可修改
        configurable : true //可删除或者修改其他特性
      })
​
      //1. 问题来了??  无法遍历
      console.log( obj.propertyIsEnumerable('sex'));
      // 解决遍历问题 
      //1.  enumerable :  true 默认是 :  false
​
      //2. 问题来了?  无法改值
      obj.sex = '男';
      // 解决问题 : 
      //2.  writable : true:,  默认是false
​
      //3. 问题又来了??
      // 不能删除或者修改其他特性(writeable除外)
      //  delete obj.sex;
      // 底层 defineProperty 定义或者修改一个属性
      // Object.defineProperty(obj , 'sex', {
      //   enumerable : false, //可遍历
      // })
      // console.log( obj.propertyIsEnumerable('sex'));
      //3. 解决办法
      //3. configurable : true
      console.log(obj);
  1. set和get

        // 底层 defineProperty 定义或者修改一个属性
        Object.defineProperty(obj, 'sex', {
          // value: '女',
          // writable : true,  // 可修改
          enumerable: true,
          configurable: true, //可修改
          set: function (newVal) {
            sex = newVal;
          },
          get: function () {
            return sex;
          }
        })
        //注意 :
        //1. value 和  writable要注释掉
        这个和get和 set冲突
        //2. 需要一个中介
        //1.修改属性的值
        obj.sex = '马哥';
        console.log(obj);
        //  obj.sex = '伟哥';
        //1. 打印可以
        console.log(obj.sex);
        //2. 遍历可以
        for (var key in obj) {
          console.log(key);
        }
        //3. 写在页面上可以
        document.write(obj.sex);
    

valueOf/toString()/toLocaleString()

    1. toString() + toLocalString()

      Object toLocaleString 返回调用 toString() 的结果。
      基本上都是一样的结果:  比如两个数组
      不一样的就是关于date的时候不一样
      
    1. valueOf() + toString()

     // toString() 和 valueOf() 的问题 
        
       // toString() 转化为字符串
          // 作用 : 以字符串的形式表示
          var arr = [1,4,7];
          var date = new Date();
          function fn() {
            console.log('fn');
          }
          var  obj = {
            name :'zs'
          }
    ​
          console.log(  arr.toString() );
          console.log(  date.toString() );
          console.log(  fn.toString() );
          console.log(  obj.toString() );
          
               // valueOf 
         // 作用 : 返回object本身 
         console.log( arr.valueOf() );
         console.log( date.valueOf() );
         console.log( fn.valueOf() );
         console.log( obj.valueOf() );
    ​
         //区别呢???
         var arr = [1,2,4];
         alert( arr.valueOf() );  //1,2, 4
         alert( arr.toString() ); // 1,2,4
         
         
         // 验证上面
         var arr = [1,2,3];  
         alert(Array.isArray(arr.valueOf()));  // true
         alert(Array.isArray(arr.toString())); //false
    

isPrototypeOf() 和 instanceOf (★)

  1. isPrototypeOf()

    • 作用 : 用于测试一个原型对象是否存在对象的原型链上。

    • 语法结构 :

      A.isPrototypeOf(B): 
      ​
      //判断A 是否存在 B 的原型链上。  说白了就是判断A是否是B的祖先
    • 演示 :

    • function Person() {
          this.name = 'zs';
      }
        var p = new Person();
      ​
        console.log( Person.prototype.isPrototypeOf(p));
      ​
        console.log( Object.prototype.isPrototypeOf(p));
  2. instanceof

    • 作用 : 作用和isPrototypeOf类似,用于判断 构造函数 的prototype属性是否在对象的原型链上。如果是,就返回true,如果不在,就返回false。

    • 语法结构 :

    • object instanceof constructor
      ​
      - 对象 instanceof  构造函数
      - 演示 :
      ​
      - ```js
        console.log( p instanceof Person);
        console.log( p instanceof Object);
      • 返回值:检测构造函数的prototype属性是否在实例对象的原型链上。

  3. 区别 :

    - A.isPrototypeOf(B)  判断A是否在B的原型链上                   A: 是一个原型对象
      //例如 : Person.prototype.isPrototypeOf(p)
    - C instanceof D      判断D的prototype是否在A的原型链上  (D是C的构造函数)    D:是一个构造函数
      //例如  p instanceof Person
    

沙箱模式

沙箱其实就是一个独立的环境,这个环境中任何的改变,都不会对外部环境产生影响。

函数自调用一样,在自调用函数内部的变量是不会影响到外部的,因此函数自调用模式也叫沙箱模式。

(function(window){
    window.food = food;
})(window);

继承

继承:子承父业

在js中的继承概念非常简单,拿来主义:一个对象自己没有的属性和方法,另一个对象有,拿过来使用,就实现了继承。

继承的目的:让一个对象可以使用另一个对象的属性和方法。

JS常见的几种继承模式:

混入式继承(mixin)

把一个对象中的属性和方法拷贝到另一个对象中。

var wg = {
    name :'玮哥',
    jiCheng : function (obj) {
​
        for (var key in obj) {
            if (obj.hasOwnProperty(key)) {
                this[key] = obj[key];
            }
        }
    }
}
wg.jiCheng(mg);
wg.jiCheng(fg);
​
缺点 : 只能给一个对象继承, 而且给自身属性添加了新的方法

原型链继承

一个对象可以访问构造函数的原型中的属性和方法,那么如果想要让一个对象增加某些属性和方法,

只需要把这些属性和方法放到原型对象中即可。这样就实现了继承, 称之为原型链继承

  • 直接给原型增加属性和方法

  • 原型替换(注意:constructor)

  • 代码 :

    //  Person.prototype.car = '法拉利';
    //  Person.prototype.price = 10000;
    var xmg = {
        constructor : Person,
        car : '法拉利',
        price : 1000
    }
    ​
    Person.prototype =  xmg;

​​ - 缺点 : 只能继承一个对象

 

mixin+原型替换

混入继承的缺点 : 引入多的方法

原型继承的缺点 : 只能继承1个

      // 原型和混入
      //1. Person.prototype
      //2. 混入 : for in 
​
    function Person() {
      this.name = '马哥';
    }
​
    Person.prototype.extend = function (obj) {
​
        for (var key in obj) {
          if (obj.hasOwnProperty(key)) {
            
              this[key] =  obj[key];
          }
        }
    }
​
    // 想要继承很多个的 但是又不能新加法伤
    var zs = {
      car : '法拉利'
    }
​
    var ls = {
      house : '海景房'
    }
​
    var ww = {
      money : 10000
    }
​
​
Person.prototype.extend(ls);
Person.prototype.extend(zs);
Person.prototype.extend(ww);
    // p1
    var p1 = new Person();
    
    console.log(p1);
​
    var  p2 = new Person();
    console.log(p2);

Object.create

最初是由道格拉斯丶克罗克福德发布的一篇文章提出的,ECMAScript5新增了Object.create()方法来规范化了这种继承。

ES5中新增了一个方法Object.create(),方法会使用指定的原型对象及其属性去创建一个新的对象。

//参数:proto 一个对象
//返回值:obj 新对象,新对象的原型就是proto
var obj = Object.create(proto);
console.log(obj);
​
var  lw = {
    car : '法拉利'
}
​
var o = Object.create(lw);
console.log(o);

call 方法借用

构造函数.call (对象A)

翻译 : 构造函数里的this ,指向了 对象A

    function Person() {
      this.name =  'zs'
    }
​
     function Student() {
      this.car =  '法拉利'
    }
​
​
    var  obj = {};
    Person.call(obj);
    Student.call(obj);
​
    console.log(obj);
    

函数进阶

定义函数的三种方式

1.函数声明

fn();//函数声明可以先调用,在声明
function fn(){
  console.log("这是函数声明")
}

2.函数表达式

var fn = function() {
  console.log("这是函数表达式");  
}
fn();//函数表达式必须先声明,再调用

3.构造函数Function

//函数也是对象,可以使用Function构造函数new出来
//相当于var fn = function(){}
var fn = new Function();
​
//语法:new Function(arg1,arg2,arg3..,body);
// 1. 所有的参数都是`字符串`类型。
// 2. 前面可以定义任意多个形参,`最后一个参数是代码体`。
  var fn1 = new Function('num1');
  var fn2 = new Function('num1','num2');
  var fn3 = new Function('num1','num2','num3');
  var fn3 = new Function('num1','num2','num3','alert(1)');

拓展1 : try...catch

// try : 尝试
// catch : 捕获
​
// 放一些可能出错的代码
// 可能出错的代码, 如果try里面出错了
// 会把错误的信息放到e里面., 执行catch,
// 如果try没有报错,,,catch不执行
​
var div = document.querySelector('div');
​
try {
​
    div.onclick =  function () {
        console.log('哈哈');
    } 
} catch (e) {
    console.log('错误了');
    console.log('请写一个div');
}
​
console.log(1111);

 

拓展2 : eval函数--了解

eval的可以和new Function一样,执行字符串代码

注意:eval函数的功能非常的强大,但是实际使用的情况并不多。

  • eval形式的代码难以阅读

  • eval形式的代码无法打断点,因为本质还是还是一个字符串

  • 在浏览器端执行任意的 JavaScript会带来潜在的安全风险,恶意的JavaScript代码可能会破坏应用

  • // 基本使用
    eval('var num1 = 20; var num2 = 30; console.log(num1+num2)');
    ​
    // 弊端 :
    eval('while(3>2) { console.log("玩我吗") }');

拓展3 : 三种读取字符串代码块的方式 + 配合eval()

// 三种读取  字符串代码块   的方式    配合 eval 
  // 方式1 : 单双引号
  var str = 'var num = 20; console.log(num)';
​
  // 方式2 : '+
  var str = 'if (3 > 2) {'+
            'console.log(2);'+
            '}';
            
  // 方式3 : 反引号
  var str =  ` if (3 > 2) {
              console.log(5);
            }`;

 

函数的四种调用模式 (★★★★★)

根据函数内部this的指向不同,可以将函数的调用模式分成4种

  1. 函数调用模式

  2. 方法调用模式

  3. 构造函数调用模式


  4. 上下文调用模式(借用方法模式)

// 学前必备
函数:当一个函数不是一个对象的属性时,我们称之为函数。
方法:当一个函数被保存为对象的一个属性时,我们称之为方法。
​
// this的指向 : 
// 谁调用this所在的函数,,this就指向谁

1.函数调用模式

如果一个函数不是一个对象的属性时,就是被当做一个函数来进行调用的。此时this指向了window

function fn(){
  console.log(this);//指向window
}
fn();

2.方法调用模式

  • 1.当一个函数被保存为对象的一个属性时,我们称之为一个方法。当一个方法被调用时,this被绑定到当前对象

var obj = {
  name : 'zs',
  sayHi:function(){
    console.log(this);//在方法调用模式中,this指向调用当前方法的对象。
  }
}
obj.sayHi();
  • 2.事件中的this指向的是当前的元素,在事件触发的时候,浏览器让当前元素调用了function

var  div = document.querySelector('div');
div.onclick = function () {
    console.log(this);
}

3.构造函数调用模式

如果函数是通过new关键字进行调用的,此时this被绑定到创建出来的新对象上。

function Person(){
  console.log(this);
}
Person();//this指向什么?
var p = new Person();//this指向什么?

总结:谁调用 this所在的函数,,,this就指向谁

//分析思路:
// 1. 看this是哪个函数的  看这个函数是谁调用的,
// 2. this 指向了谁 ? 
​
// 准备1
var age = 38; 
var obj = {
    age: 18,  
}
​
// 准备2
var  obj = {
  name : 'zs'
}
console.log(obj.name);
console.log(obj['name']);
​
// 准备3
function fn() {
  console.log('调用了');
  console.log(this);
  console.log(this.length);
}
var arr = [fn];
arr[0]();  
// 相当于 arr.0()  如同 obj['name'] == obj.name 一样
// arr数组调用的
​

4.上下文调用模式 (方法借用)

上下文调用模式也叫方法借用模式,分为apply与call

使用方法: 函数.call() 或者 函数.apply()

call方法

call方法可以调用一个函数,并且可以指定这个函数的this指向

//1. 所有的函数都可以使用call进行调用
function fn() {
    console.log('哈');
    console.log(this);
}
fn();
// 1. 没有参数
fn.call();
// 2. 有参数的
// 语法 : 
// call(参数1,参数2,参数3..)
// 参数1 : this 指向了第一个参数, (如果第一个参数为null,默认指向windw)
// 其余参数 : 就相当于函数的参数一样
function f() {
    console.log(this);
    console.log(arguments);  
}
var obj = {};
f.call(obj,1,2);  // 相当于 obj.call(1,2)
​
//说白了,call方法也可以和()一样,进行函数调用,call方法的第一个参数可以指定函数内部的this指向。
//一般情况下 :  应用场景 
//继承 :  函数.call(对象) 
//方法借用 : 方法.call(对象,参数,,,) ==> 对象.方法()
fn.call(thisArg, arg1, arg2, arg2);
​
例1 : fn.call([], 1, 2);
例2 : 
var  obj = {
  name : '达达',
  liaomei : function () {
    console.log( this.name + '只会撩妹');
  }
}
var mg = {
  name : 'mg'
};
obj.liaomei.call(mg); // mg.liaomei();
  • 借用对象的方法

伪数组与数组

伪数组也叫类数组

  1. 伪数组其实就是一个对象,但是跟数组一样,伪数组也会有length属性,也有0,1,2,3等属性。

  2. arguments 的proto 和 var arr = [] 的 __ proto __ 比较

  3. 伪数组并没有数组的方法,不能使用push/pop等方法

  4. 伪数组可以跟数组一样进行遍历,通过下标操作。

  5. 常见的伪数组:argumentsdocument.getElementsByTagName的返回值jQuery对象

var obj = {
  0:"张三",
  1:"李四",
  2:"王五",
  length:3
}
//伪数组可以和数组一样进行遍历
  • 伪数组借用数组的方法

Array.prototype.push.call(obj, "赵六");
var str = Array.prototype.join.call(obj, "-");
  • 将伪数组转换成真数组

var arr = Array.prototype.slice.call(obj);

apply方法

apply()方法的作用和 call()方法类似,只有一个区别,就是apply()方法接受的是一个包含多个参数的数组。而call()方法接受的是若干个参数的列表

  //apply和call一样,都可以调用函数,区别,参数不一样
​
  //apply语法:
  //参数1:指定this
  //参数2:数组 ,需要把所有的形参都放到一个数组中

call和apply的使用场景:

  • 如果参数比较少,使用call会更加简洁

  • 如果参数存放在数组中,此时需要使用apply

  • 函数.call(this的指向目标, 参数1,参数2)   参数列表
    函数.apply(this的指向目标, [参数1,参数2]) 参数素组

bind方法 (了解)

bind()方法创建一个新的函数, 可以绑定新的函数的this指向

//返回值:新的函数
//参数:新函数的this指向,当绑定了新函数的this指向后,无论使用何种调用模式,this都不会改变。
function fn() {
    console.log(this);
}
​
var dd = {
    name : 'zs'
}
​
// 绑定,返回一个新函数
var newFn =  fn.bind(cc);
newFn();
​
​
// 即使再让别的对象调用,也不会改变
var obj = {
    f : newFn
}
obj.f();

函数也是对象

函数是由new Function创建出来的,因此函数也是一个对象, 所有的函数都是new Function的实例

函数的原型链结构

画出下列代码的原型链结构

//var Person = new Function();
function Person(){
  
}

Function.prototype成员

  • arguments:获取函数的实参,被函数内部的arguments替代了。 (废弃)

  • length:获取形参的长度

  • name:获取函数的名字,此属性不允许修改

  • caller:用于获取当前在函数是在哪个函数中调用的,已经被废弃了。(废弃)

  • constructor:指向当前构造函数,Function

  • call:调用函数,重新指向this

  • apply:调用函数,重新指向this

  • bind:重新指向this,返回一个新的函数,不调用。

 

总结:

  1. 所有函数都是new Function创建出来的,因此所有函数.__proto__都是Function.prototype

  2. 所有对象都是new Object创建出来的,因此所有对象.__proto__都是Object.prototype

 

预解析与作用域

预解析

预解析:预先解析

js执行代码分为两个过程: (记忆)

  • 预解析过程(变量与函数提升)

  • 代码一行一行执行

预解析过程:JavaScript解析器在执行代码前,会把所有变量的声明和函数的声明提升到当前作用域的顶部。例如var a = 11;其实会分为var a;a = 11两部分,其中var a;会被提升。

预解析规则:

  1. 把var 声明的变量提升,,赋值不提升;

    包括 : var num = 20;   var fn = function() {...} 
  2. 函数声明(不是表达式), 整体提升

    包括 :  
    function test() {
    }
  3. 函数同名,后者覆盖前者

  4. 函数与变量同名,,函数覆盖变量

推荐:不要在一个作用域内重复的声明相同的变量和函数

作用域

作用域:变量起作用的区域,作用域决定了一个变量被定义在哪里,以及该如何被查找。

  • 全局作用域 : 函数外部的区域;

  • 函数作用域 : 函数内部的区域;

全局变量:在函数外定义的变量就叫全局变量,全局变量在任何地方都能访问到。

局部变量:在函数内定义的变量就叫局部变量,局部变量只有在当前函数内才能访问到。

隐式全局变量 : 只赋值未声明的变量, 隐式全局也是全局;

var num = 11;//全局变量
function fn(){
  var num1 = 22;//局部变量
  console.log(num1);
  num3 = 44;
}
console.log(num);

编程语言中,作用域规则分为两种:

  • 词法作用域(静态作用域)

  • 动态作用域

JavaScript采用的是词法作用域规则,词法作用域也叫静态作用域,变量在函数声明的时候,它的作用域就定下来了,与函数的调用无关。

var num = 123;
function f1() {
  console.log(num);
}
​
function f2(){
  var num = 456;
  f1();
}
f2();//打印啥?

作用域链

作用域链:只要是函数,就会形成一个作用域,如果这个函数被嵌套在其他函数中,那么外部函数也有自己的作用域,这个一直往上到全局环境,就形成了一个条作用域链。

变量的搜索原则

  1. 从当前作用域开始搜索变量,如果存在,那么就直接返回这个变量的值。

  2. 如果不存在,就会往上一层作用域查询,如果存在,就返回。

  3. 如果不存在,一直查询到全局作用域,如果存在,就返回。如果不存在说明该变量是不存在的。

  4. 如果一个变量不存在

    • 获取这个变量的值会报错xxx is not defined;

    • 给这个变量设置值,那么设置变量就是隐式全局变量

 

函数闭包 ★

闭包(closure)是JavaScript语言的一个难点,也是JavaScript的一个特色,很多高级的应用都要依靠闭包来实现。

为什么要学习闭包??

    // 1.里面可以访问外面的变量
    var num = 10;
    function outer() {
      console.log(num);
    }
    outer();
​
    //2. 外面想访问里面的变量怎么办?
    function outer() {
      var num = 10;
      return num; // num + 20;
    }
    var res = outer();
    console.log(res);
    //3. 外面想操作修改这个变量怎么办??
    // 上面的例子只是返回,有多少返回多少,就算你在这里运算也是
    // 返回多少就是多少,也是死的
    function outer() {
      var num = 10;
      function inner(n) {
        num = n;
        console.log(num);
      }
      // 这里依然是内部操作者的
      inner(50)
    }
     outer();
​
    //4. 终极版 : 外面想操作函数内部的数据
    function outer() {
      var num = 10;
      function inner(n) {
        num = n;
        console.log(num);
      }
      return inner;
    }
   var f =  outer();
   f(60);

 

闭包的概念

百度百科 : 闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

维基百科 : 闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数 

总结 : 在JavaScript中,在外部函数中嵌套另一个子函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。

闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用

产生闭包的条件

// 当内部函数访问了外部函数的变量的时候,就会形成闭包。
// scope 作用域
// 断点调试
​
function outer() {
    var num = 10;
    function inner() {
​
        console.log(num); // 产生
        console.log(11);  // 不产生
    }
    return  inner;
}
​
outer()();

闭包的应用

计数器闭包的应用

需求:统计一个函数的调用次数

var count = 0;
function fn(){
  count++;
  console.log("我被调用了,调用次数是"+count);
}
fn();
fn();
fn();

缺点:count是全局变量,不安全。

使用闭包解决这个问题!!!!

function outer(){
  var count = 0;
  function add(){
    count++;
    console.log("当前count"+count);
  }
  return add;
}
​
var result = outer();
result();
result();
result();

闭包的作用:

  1. 把变量保护起来 ()

  2. 这些变量的值始终保持在内存中 ==> 应用

私有变量

使用闭包实现私有变量的读取和设置

function outer(){
​
  var num = 10;
​
  function set_num(n){
    num = n;
  }
​
  function get_num(){
    return num;
  }
​
  return {
    set_num:set_num,
    get_num:get_num
  }
}
​
var obj = outer();
obj.set_num(2000);
console.log(obj.get_num());

闭包存在的问题

闭包占用的内存是不会被释放的,因此,如果滥用闭包,会造成内存泄漏的问题。闭包很强大,但是只有在必须使用闭包的时候才使用。

js的垃圾回收机制

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management

  • 内存:计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大,运行程序需要消耗内存,当程序结束时,内存会得到释放。

  • javascript分配内存:当我们定义变量,javascript需要分配内存存储数据。无论是值类型或者是引用类型,都需要存储在内存中。

  • 垃圾回收:当代码执行结束,分配的内存已经不需要了,这时候需要将内存进行回收,在javascript语言中,垃圾回收机器会帮我们回收不再需要使用的内存。

引用记数法清除

引用记数垃圾收集:如果没有引用指向某个对象(或者是函数作用域),那么这个对象或者函数作用域就会被垃圾回收机制回收。

var o = {
  name:"zs"
}
//对象被o变量引用  ,引用记数1
var obj = o;   //变量被o和obj引用,引用记数2
​
o = 1;  //o不在引用对象了, 引用记数1
obj = null; //obj不在引用对象了,引用记数0,可以被垃圾回收了。

标记清除法清除

使用引用计数法进行垃圾回收的时候,会出现循环引用导致内存泄漏的问题。因此现代的浏览器都采用标记清除法来进行垃圾回收。

这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象Window)。定期的,垃圾回收器将从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象。

闭包占用内存释放

function outer(){
  var count = 0;
​
  function fn(){
    count++;
    console.log("执行次数"+count);
  }
  return fn;
}
​
​
var result = outer();
result();
result = null;//当函数fn没有被变量引用了,那么函数fn就会被回收,函数fn一旦被回收,那么outer调用形成的作用域也就得到了释放。

 

正则表达式

正则表达式:用于匹配规律规则的表达式,正则表达式最初是科学家对人类神经系统的工作原理的早期研究,现在在编程语言中有广泛的应用,经常用于表单校验,高级搜索等。

学习要求 : 能识别各种正则表达式; 会使用编写简单的正则表达式

创建正则表达式

构造函数的方式

var regExp = new RegExp(/\d/);

正则字面量

var regExp = /\d/;

正则的使用

// test 是正则的其中一个常用的方法
// 结构 : 正则.test('要检查的字符串')
// 返回值 : true/false
​
/\d/.test("aaa1");

元字符

正则表达式由一些普通字符和元字符组成,普通字符包括大小写字母、数字等,而元字符则具有特殊的含义。

1.常见元字符

 

特殊字符换 http://www.w3school.com.cn/js/js_special_characters.asp

优先级 : | 和 ()

| 表示或,优先级最低

()优先级最高,表示分组

// 优先级  |  ()
// | 
console.log(/foot|boot/.test('bootfoot'));
console.log(/foot|boot/.test('bootf'));
console.log(/foot|boot/.test('bfoot'));
​
// () 
console.log(/(f|b)oot/.test('bootfoot'));
console.log(/(f|b)oot/.test('bootf'));
console.log(/(f|b)oot/.test('bfoot'));
console.log(/(f|b)oot/.test('bfot'));

 

2.字符类的元字符 : [] 和 [^] 和 [a-z]

[]在正则表达式中表示一个字符的位置,[]里面写这个位置可以出现的字符。

// 字符里包含a | b | c => 都为true
console.log(/[abc]/.test('a123'));

[^]在中扩号中的^表示 非,除了 的意思。

判断 : 是否包括除了XX以外的字符串,如果包含了除了XX以外的字符都可以为true

  //[^0]:除了0以外的字符
  console.log(/[^0]/.test("1aaa000"));
  console.log(/[^0]/.test("a"));

[a-z] [1-9]表示范围

console.log(/[a-z]/.test("d"));//小写字母
console.log(/[A-Z]/.test("d"));//大写字母
console.log(/[0-9]/.test("8"));//数字
console.log(/[a-zA-Z0-9]/);//所有的小写字母和大写字母以及数字

3.边界类元字符 ^ 和 $

我们前面学习的学习的正则只要有满足的条件的就会返回true,并不能做到精确的匹配。

^表示开头   []里面的^表示取反

$表示结尾

console.log(/^chuan/.test("dachuan"));//必须以chuan开头
console.log(/chuan$/.test("chuang")); //必须以chuan结尾
console.log(/^chuan$/.test("chuan")); //精确匹配chuan
​
//精确匹配chuan,表示必须是这个
console.log(/^chuan$/.test("chuanchuan"));//fasle

4.量词类元字符

注意 : 量词用来控制出现的次数,一般来说 量词边界 一起使用

切记 : 量词 和 精确匹配 一起使用

  //量词用来控制出现次数的
  1. *表示能够出现0次或者更多次,x>=0;

     console.log(/a*/.test('123'));
  2. +表示能够出现1次或者多次,x>=1

    console.log(/^a+$/.test('aaaa'));
  3. ?表示能够出现0次或者1次,x=0或者x=1

    var str="1, 100 or 1000?"; 
    var pat=/10?/g;
    console.log(str.match(pat));
  4. {n}表示能够出现n次

  5. {n,}表示能够出现n次或者n次以上

  6. {n,m}表示能够出现n-m次

思考:如何使用{}来表示*+?

 

正则的使用

学习目的

正则测试

  1. 验证座机

    • 比如010-12345678 0797-1234567

    • 开头是3-4位,首位必须是0

    • -后面是7-8位

    var phoneReg = /^0\d{2,3}-\d{7,8}$/;
  2. 验证姓名

    • 只能是汉字 \u4e00-\u9fa5

    • 长度2-6位之间

    • 汉字范围[\u4e00-\u9fa5]

    var nameReg = /^[\u4e00-\u9fa5]{2,6}$/;
  3. 验证QQ

    • 只能是数字

    • 开头不能是0

    • 长度为5-11位

    var qqReg = /^[1-9]\d{4,10}$/;
  4. 验证手机

    • 11位数字组成

    • 号段13[0-9] 147 15[0-9] 17[0178] 18[0-9]

    var mobileReg = /^(13[0-9]|147|15[0-9]|17[0178]|18[0-9])\d{8}$/;
  5. 验证邮箱

    [email protected]
    [email protected]
    • 前面是字母或者数字

    • 必须有@

    • @后面是字母或者数字

    • 必须有.

    • .后面是字母或者数字

    var emailReg = /^\w+@\w+(\.\w+)+$/;

正则替换 replace (字符串方法)

var str = " 123AD  asadf   asadfasf  adf  ";
1  替换掉字符串中的所有空白
2. 将所有的ad替换成xx   i:忽略大小写
3. 将所有的ad/AD替换成xx
​
var str = 'abc,efg,123,abc,123,a'
4. 所有的逗号替换成叹号
​
var jsonStr = '[{"name":"张三",score:80},{"name":"张三",score:90},{"name":"张三",score:81}]'; 
5. 把所有成绩都修改成100分
​
var  str =  jsonStr.replace(/\d+/g,'100');

 

正则匹配 match (字符串方法) =>数组

//1. 
var str = "我的手机号是:18511241111, 我的女朋友的手机号是:13211111111,我的前女友的手机号是:18522223333,我的前前女友的手机号是:18511112293";
//需求:把字符串中所有的手机号找出来。
var arr = str.match(/(13[0-9]|147|15[0-9]|17[0178]|18[0-9])\d{8}/g);
//结果 : ["18511241111", "13211111111", "18522223333", "18511112293"]
​
//2. 回顾 ? 
  var str = '1,10,100,1000';
  var arr = str.match(/10?/g);
  console.log(arr);

正则提取 exec (正则方法)=> 数组

//今天是2018-05-11, 要求;提取得到年月日;
var str = "今天是2018-05-11, 要求;得到年月日"
​
// 提取
// var reg = /(\d{4})-(\d{2})-(\d{2})/;
var reg = /\d{4}-\d{2}-\d{2}/;

 

字符串总结:

//正则表达式只有:2个: test:测试 exec:提取

reg.test()

reg.exec()

//字符串有两个方法: replace match: 支持参数传正则

str.replace()

str.match();

  //正则的使用:
    //1. 字符串的replace: 正则的替换
    //2. 字符串的匹配:match:  匹配某个字符串中所有符合规律的字符串。
    //3. 正则的测试:test:   表单校验,判断某个字符串是否符合正则的规律
    //4. 正则的提取: 提取匹配的字符串的每一个部分。  ()进行分组

 

你可能感兴趣的:(前端之路(JS高级篇))