JavaScript高级(1)

JavaScript高级(1)

回顾

JavaScript是什么

  • 解析执行:轻量级解释型
  • 语言特点:动态、头等函数(First-class-Function)
    – 又称函数是JavaScript一等公民
  • 执行环境:在宿主环境下运行,浏览器是最常见的JavaScript宿主环境
    – 但是在很多非浏览器环境中也使用JavaScript,例如node.js

JavaScript的组成

  • ECMAscript 语法规范
    • 变量、数据类型、类型转换、操作符
    • 流程控制语句:判断、循环语句
    • 数组、函数、作用域、预解析
    • 对象、属性、方法、简单类型 和复杂类型的区别
    • 内置对象:Math、Date、Array,基本包装类型String、Number、Boolean
  • Web API
    • BOM
      • onload页面加载事件,window顶级对象
      • 定时器
      • localtion、history
    • DOM
      • 获取页面元素,注册事件
      • 属性操作,样式操作
      • 节点属性,节点层级
      • 动态创建元素
      • 事件:注册事件的方式,事件的三个阶段、事件对象

JavaScript面向对象编程

什么是对象
  1. 对象是单个事物的抽象,比如一本书,一个人…
  2. 对象是一个容器,封装了属性和方法
    • 属性是对象的状态,方法是行为。比如,我们可以把动物抽象为animal对象,使用属性来记录是什么动物,使用方法表示动物的某种行为(吃饭,跑步)
什么是面对对象

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

面对对象与面向过程
  • 面向过程就是亲力亲为,事物巨细面面俱到,有条不紊
  • 面向对象就是找一个对象,只会得结果
  • 面向对象将执行者转变成指挥者
  • 面向对象不是面向过程的替代,而是面向过程的封装
面向对象的特性
  • 封装性
  • 继承性
  • 多态性(抽象)

程序中面向对象的基本体现

在JavaScript中,所有数据类型都可以视为对象,当然也可以自定义对象
自定义对象数据类型就是面对对象中类发概念
假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个对象表示:

var std1 = { name: 'Michael', score: 98 }
var std2 = { name: 'Bob', score: 81 }

而处理学生的成绩可以通过函数实现,比如打印学生的成绩:

function printScore (student) {
  console.log('姓名:' + student.name + '  ' + '成绩:' + student.score)
}
抽象数据行为模板(class):
function Student(name, score) {
  this.name = name;
  this.score = score;
  this.printScore = function() {
    console.log('姓名:' + this.name + '  ' + '成绩:' + this.score);
  }
}

根据模板创建具体实例对象(instance):

var std1 = new Student('Michael', 98)
var std2 = new Student('Bob', 81)

实例对象具有自己的具体行为(给对象发消息)

std1.printScore() // => 姓名:Michael  成绩:98
std2.printScore() // => 姓名:Bob  成绩 81

面向对象的设计思想是:

  • 抽象出class(构造函数)
  • 根据class(构造函数)创建instance
  • 指挥instance得到结果
    面向对象的抽象程度又比函数要高,因为一个class既包含数据又包含操作数据的方法

创建对象

简单方式

new object()

var person = new Object()
person.name = 'Jack'
person.age = 18

person.sayName = function () {
  console.log(this.name)
}

每次创建通过new object()比较麻烦,所以可以通过它的简写形式对象字面量来创建

var person={
   name:'jack',
   age:18,
   sayNmae:function(){
      console.log(this.name);
   }
}

对于上面的写法固然没有问题,但是假如我们要生成两个person对象?

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

var person2 = {
  name: 'Mike',
  age: 16,
  sayName: function () {
    console.log(this.name)
  }
}

简单方式的改进 :工厂函数

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)

构造函数

  • 构造函数语法
  • 分析构造函数
  • 构造函数和实例对象的关系
    • 实例的constructor属性
    • instanceof操作符
  • 普通函数调用和构造函数调用的区别
  • 构造函数的返回值
  • 构造函数的问题

更优雅的工厂函数 :构造函数

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

person()和creatPerson()的不同

  • 没有显示的创建对象
  • 直接将属性和方法赋给了this对象
  • 没有return对象
  • 函数名使用的是大写的person
    调用构造函数的四个步骤:
    1.创建一个新对象
    2.将构造函数的作用域赋给新对象(因此this就指向新对象)
    3.执行构造函数中的代码
    4.返回新对象
function Person (name, age) {
  // 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象
  // var instance = {}
  // 然后让内部的 this 指向 instance 对象
  // this = instance
  // 接下来所有针对 this 的操作实际上操作的就是 instance

  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }

  // 在函数的结尾处会将 this 返回,也就是 instance
  // return this
}

构造函数和实例对象的关系

使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型
在每一个实例对象中同时有一个constructor属性,该属性指向创建改实例的构造函数

console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true

对象的constructor属性最初是用来标识对象类型的,但是如果要检测对象的类型,还是使用instanceof操作符更可靠

console.log(p1 instanceof Person) // => true
console.log(p2 instanceof Person) // => true

总结:

  • 构造函数是根据具体的事物抽象出来的抽象模板
  • 实例对象是根据抽象的构造函数模板得到的具体实例对象
  • 每一个实例对象都具有一个constructor属性,指向创建该实例的构造函数
    • 注意:constructor是实例的属性的说法不严谨,具体后面的原型
  • 可以通过实例的constructor属性判断实例和构造函数之间的关系(不严谨,推荐使用instanceof)

构造函数的问题

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

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('Tom', 18)
var p2 = new Person('Jack', 16)

上面的示例每一个type和sayhello都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存如果实例对象很多,会造成极大的内存浪费。

console.log(p1.sayHello === p2.sayHello) // => false

对于这种问题我们可以定义到构造函数外部

function sayHello = function () {
  console.log('hello ' + this.name)
}

function Person (name, age) {
  this.name = name
  this.age = age
  this.type = 'human'
  this.sayHello = sayHello
}

var p1 = new Person('Top', 18)
var p2 = new Person('Jack', 16)

console.log(p1.sayHello === p2.sayHello) // => true

这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题

var fns = {
  sayHello: function () {
    console.log('hello ' + this.name)
  },
  sayAge: function () {
    console.log(this.age)
  }
}

function Person (name, age) {
  this.name = name
  this.age = age
  this.type = 'human'
  this.sayHello = fns.sayHello
  this.sayAge = fns.sayAge
}

var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)

console.log(p1.sayHello === p2.sayHello) // => true
console.log(p1.sayAge === p2.sayAge) // => true

小结:

  • 构造函数语法
  • 分析构造函数
  • 构造函数和实例对象的关系
    • 实例的constructor属性
    • instanceof 操作符
  • 构造函数的问题

原型

  • 使用prototype原型对象解决构造函数的问题
  • 分析构造函数、prototype原型对象。实例对象三者之间的关系
  • 属性成员搜索原则:原型链
  • 实例对象读写原型对象中的成员
  • 原型对象的简写形式
  • 原生对象的原型
    • Object
    • Array
    • String
  • 原型对象的问题
  • 构造函数和原型对象使用建议

更好的解决办法:prototype

JavaScript规定:每一个构造函数都有一个prototype属性指向另一个对象。
这个对象的所有属性和方法,都会被构造函数的所拥有

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

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对象,因此提高了运行效率

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

在这里插入图片描述
任何函数都具有一个prototype属性,该属性是一个对象

function F () {}
console.log(F.prototype) // => object

F.prototype.sayHi = function () {
  console.log('hi!')
}

构造函数的prototype对象默认都有一个constructor属性,指向prototype对象所在函数。

console.log(F.prototype.constructor === F) // => true

通过构造函数得到的实例对象内部都会包含一个指向构造函数的prototype对象的指针_ _proto _ _

var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true

总结:

  • 任何函数都具有一个prototype属性,该属性是一个对象
  • 构造函数的prototype对象默认都有一个constructor属性,指向prototype对象所在函数
  • 通过构造函数得到的实例对象内部会包含一个指向构造函数的prototype对象的指针_ _ proto_ _
  • 所有实例都直接或间接继承了原型对象的成员

属性成员的搜索原则:原型链

  • 搜索首先从对象实例本身开始
  • 如果在实例中找到了具有给定名字的属性,则返回该属性的值
  • 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
  • 如果在原型对象中找到了这个属性,则返回该属性的值
    总结:
  • 先在自己身上找,找不到即返回
  • 自己身上找不到,则沿着原型链向上查找,找到即返回
  • 如果一直到原型链的末端还没有找到,则返回undefined

实例对象读写原型对象成员

读取:

  • 先在自己身上找,找不到即返回
  • 自己身上找不到,则沿着原型链向上查找,找到即返回
  • 如果一直到原型链的末端还没有找到,则返回undefined
    值类型成员写入(实例对象,值类型成员=xx)
  • 当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
  • 也就是说该行为实际上会屏蔽掉对原型对象成员的访问
    引用类型成员写入(实例对象.引用类型成员=xx)
  • 同上
    复杂类型修改(实例对象.成员.xx=xx)
  • 同样会先在自己身上找改成员,如果自己身上找到则直接修改
  • 如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
  • 如果一直到原型链的末端还没有找到该成员,则报错(实例对象.undefined.xx=xx)

最简单的原型语法

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

Person.prototype = {
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
  }
}

在该示例中,我们将person.prototype重置到一个新的对象
这样做的好处就是为person.prototype添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了constructor成员

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

Person.prototype = {
  constructor: Person, // => 手动将 constructor 指向正确的构造函数
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
  }
}

原生对象的原型
所有函数都有prototype属性对象

  • Object.prototype
  • Function.prototype
  • Array.prototype
  • String.prototype
  • Number.prototype
  • Date.prototype

原型对象使用建议

  • 私有成员(一般就是非函数成员)放到构造函数中
  • 共享成员(一般就是函数)放到原型对象中
  • 如果重置了prototype记得修正constructor的指向

继承

构造函数的继承:借用构造函数
function Person (name, age) {
  this.type = 'human'
  this.name = name
  this.age = age
}

function Student (name, age) {
  // 借用构造函数继承属性成员
  Person.call(this, name, age)
}

var s1 = Student('张三', 18)
console.log(s1.type, s1.name, s1.age) // => human 张三 18
构造函数的原型方法继承:拷贝继承(for-in)
function Person (name, age) {
  this.type = 'human'
  this.name = name
  this.age = age
}

Person.prototype.sayName = function () {
  console.log('hello ' + this.name)
}

function Student (name, age) {
  Person.call(this, name, age)
}

// 原型对象拷贝继承原型对象成员
for(var key in Person.prototype) {
  Student.prototype[key] = Person.prototype[key]
}

var s1 = Student('张三', 18)

s1.sayName() // => hello 张三
另一种继承方式:原型继承
function Person (name, age) {
  this.type = 'human'
  this.name = name
  this.age = age
}

Person.prototype.sayName = function () {
  console.log('hello ' + this.name)
}

function Student (name, age) {
  Person.call(this, name, age)
}

// 利用原型的特性实现继承
Student.prototype = new Person()

var s1 = Student('张三', 18)

console.log(s1.type) // => human

s1.sayName() // => hello 张三

函数进阶

函数的定义方式
  • 函数声明
  • 函数表达式
  • new Function
函数声明
function foo () {

}
函数表达式
var foo = function () {

}
函数声明与函数表达式的区别
  • 函数声明必须有名字
  • 函数声明会函数提升,在预解析阶段就已创建,声明前后都可以调用
  • 函数表达式类似于变量赋值
  • 函数表达式可以没有名字,例如匿名函数
  • 函数表达式没有变量提升,在执行阶段创建们必须在表达式执行之后才可以调用
if (true) {
  function f () {
    console.log(1)
  }
} else {
  function f () {
    console.log(2)
  }
}

函数的调用方式

  • 普通函数
  • 构造函数
  • 对象方法

函数内this指向不同的场景

调用方式this指向
普通函数调用window
构造函数调用实例对象
对象方法调用该方法所属对象

函数也是对象

所有函数都是function的实例

call

call()方法调用一个函数,其具有一个指定的this值和分别地提供参数
注意:该方法的作用和apply()方法有点类似,只有一个区别,就是call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组
语法:

fun.call(thisArg[, arg1[, arg2[, ...]]])

参数:

  • thisArg
    • 在fun函数运行时指定this值
    • 如果指定了null或者undefined则内部this指向window
  • arg1,arg2,…
    • 指定的参数列表
apply

apply()方法调用一个函数,其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数。
语法:

fun.apply(thisArg, [argsArray])
参数:

  • thisArg
  • argArray
    apply()使用参数数组而不是一组参数列表
fun.apply(this, ['eat', 'bananas'])
bind

bind() 函数会创建一个新函数,新函数与被调函数具有相同的函数体
在目标函数被调用时this值绑定到bind()的第一个参数,该参数不能被重写。绑定函数被调用时,bind()也接受预设的参数提供给原函数
一个绑定函数也能使用new操作符创建对象,这种行为就像把原函数当成构造器。提供的this值被忽略,同时调用时的参数被提供给模拟函数。
语法:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

参数:

  • thisArg
    • 当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用new 操作符调用绑定函数时,该参数无效。
  • arg1, arg2, …
    • 当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。

返回值:

返回由指定的this值和初始化参数改造的原函数拷贝。
示例:

this.x = 9; 
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX(); // 返回 81

var retrieveX = module.getX;
retrieveX(); // 返回 9, 在这种情况下,"this"指向全局作用域

// 创建一个新函数,将"this"绑定到module对象
// 新手可能会被全局的x变量和module里的属性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81
function LateBloomer() {
  this.petalCount = Math.ceil(Math.random() * 12) + 1;
}

// Declare bloom after a delay of 1 second
LateBloomer.prototype.bloom = function() {
  window.setTimeout(this.declare.bind(this), 1000);
};

LateBloomer.prototype.declare = function() {
  console.log('I am a beautiful flower with ' +
    this.petalCount + ' petals!');
};

var flower = new LateBloomer();
flower.bloom();  // 一秒钟后, 调用'declare'方法

应用

  • 让伪数组使用数组的方法var obj = { 0: 1, 1: 8, 2: 10, 3: 3, 4: 2, length: 5 }; Array.prototype.splice.call(obj, 0, 1); Array.prototype.push.call(obj, 100); console.log(obj);
  • 对象内部使用使用定时器var o = { name: 'hanmeimei', func1: function () { console.log(this.name); }, func2: function () { setInterval(function () { console.log(this); }.bind(this), 2000); } }; o.func2();
  • 让数组的每一项作为方法的参数var arr = [2, 1, 8, 9, 10, 3]; console.log(Math.max.apply(Math, arr)); console.log.apply(console, arr);

小结

  • call 和 apply 特性一样
    • 都是用来调用函数,而且是立即调用
    • 但是可以在调用函数的同时,通过第一个参数指定函数内部 this 的指向
    • call 调用的时候,参数必须以参数列表的形式进行传递,也就是以逗号分隔的方式依次传递即可
    • apply 调用的时候,参数必须是一个数组,然后在执行的时候,会将数组内部的元素一个一个拿出来,与形参一一对应进行传递
    • 如果第一个参数指定了 null 或者 undefined 则内部 this 指向 window
  • bind
    • 可以用来指定内部 this 的指向,然后生成一个改变了 this 指向的新的函数
    • 它和 call、apply 最大的区别是:bind 不会调用
    • bind 支持传递参数,它的传参方式比较特殊,一共有两个位置可以传递
      1. 在 bind 的同时,以参数列表的形式进行传递
      2. 在调用的时候,以参数列表的形式进行传递
      • 那到底以谁 bind 的时候传递的参数为准呢还是以调用的时候传递的参数为准
      • 两者合并:bind 的时候传递的参数和调用的时候传递的参数会合并到一起,传递到函数内部

发表评论