什么是对象
万物皆对象
我们可以从两个层次来理解
(1)对象是单个事物的抽象
(2)对象是一个容器,封装来属性(property)和方法(method)
属性:对象的状态
方法:对象的行为在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集功能集。
ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值,对象或者函数
什么是面向对象
- 面向对象编程 ---- Object Oriented Programming 简称 OOP,是一种编程开发思想
- 它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟
面向对象与面向过程对比
- 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
- 面向对象就是找一个对象,指挥得结果
- 面向对象将执行者转变成指挥者
- 面向对象不是面向过程的替代,而是面向过程的封装
面向对象的特性
- 封装
- 继承
- 多态(抽象)
总结
- 在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接收信息,处理数据、发出信息等任务
- 因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程,更适合多人合作的大型软件项目
面向对象的设计思想
- 抽象出 class (构造函数)
- 根据 Class(构造函数) 创建 Instance ( 实例)
- 指挥 Instance 得出结果
创建对象的几种方法
- new Object() 构造函数
// 简单方式 new Object()
var person = new Object();
person.name = "tin";
person.age = 18;
person.sayName = function() {
console.log(this.name)
}
- 对象字面量 {}
// 对象字面量化简
var person = {
name: "tin",
age = 19,
sayName : function(){
console.log(this.name)
}
}
- 工厂函数
// 工厂函数
function createPerson(name,age){
//添加一个新对象
var person = new Object();
person.name = name;
person.age = age;
person.sayName = function(){
console.log(this.name);
}
// 必须有返回值
return person
}
//生产真正的对象
var person1 = createPerson("bob",17);
var person2 = createPerson("tin",18);
- 自定义构造函数
// 自定义构造函数
function Person(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name)
}
}
// 生成对象实例
var person1 = new Person("tin",19);
var person2 = new Person("bob",18);
person1.sayName();
person2.sayName();
new 关键字的用途
1 创建一个新对象
2 将函数内部的 this 指向这个新对象
3 执行构造函数内部的代码
4 将新对象作为返回值
通过构造函数生成的实例是可以找到自己当初的构造函数的
constructor 属性 构造器 构造函数
每个对象的 constructor 属性值就是生成这个对象的构造函数
判断一个对象的具体类型,需要使用 instance of 进行判断
构造函数和实例对象的关系
- 构造函数是根据具体事物抽象出来的抽象模版
- 实例对象是根据抽象的构造函数模版得到的具体实例对象
- 每一个实例对象都通过一个 constructor 属性,指向创建该实例的构造函数
注意:constructor 是实例的属性的说法不严谨,具体后面的原型会讲到 - 可以通过 constructor 属性判断实例和构造函数之间的关系
注意:这种方式不严谨,推荐使用 instanceof 操作符
静态成员和实例成员
- 使用构造函数方法创建对象时,可以给构造函数和创建的实例对象添加属性和方法,这些属性和方法都叫做成员
- 实例成员:在构造函数内部添加给 this 的成员,属于实例对象的成员,在创建实例对象后必须由对象调用。
- 静态成员:添加给构造函数自身的成员,只能使用构造函数调用,不能使用生成的实例对象调用
构造函数的问题
- 浪费内存
// 解决办法1:将公共的函数提取到构造函数之外
function sayName(){
console.log(this.name);
}
// 解决办法2:将多个公共的函数封装到一个对象
var fns = {
sayName: function(){
console.log(this.name);
},
sayAge: function(){
console.log(this.age)
}
}
原型对象
- 使用原型对象可以更好的解决构造函数的内存浪费问题
prototype 原型对象
- 任何函数都具有一个 prototype 属性,该属性是一个对象
- 可以在原型对象上添加属性和方法
- 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在的函数
- 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 prototype
- 实例对象可以直接访问原型对象成员
// 定义构造函数
function Person(name,age){
this.name = name;
this.age = age;
}
// 获取构造函数的 prototype 属性
console.log(Person.prototype);
// 属性值是一个对象,通常叫做原型对象
// 对象内部可以添加一些属性和方法
Person.prototype.type = "human";
Person.prototype.sayHi = function(){
console.log("hello");
};
// 构造函数的原型对象上面都默认有一个 constructor 属性
console.log(Person.prototype.constructor);
// 创建实例对象
var p1 = new Person("mike",19);
// 所有对象都有一个 _proto_ 的属性,是一个指针,指向的就是生成实例对象的原型对象
console.log(p1._proto_)
// _proto_ 属性并不是一个标准的属性,是浏览器自己根据语法自动生成
// 在真正的开发中,是不会书写_proto_ 属性的
p1.sayHi();
解决方法
- JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向构造函数的原型对象
- 这个原型对象的所有属性和方法,都会被构造函数的实例对象所拥有
- 因此我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上
- 解决内存浪费问题
原型链
原型链查找机制
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标时具有给定名字的属性
1 搜索首先从对象实例本身开始
2 如果在实例中找到链具有给定名字的属性,则返回该属性的值
3 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
4 如果在原型对象中找到链这个属性,则返回该属性的值
实例对象读写原型对象成员
值类型成员写入(实例对象,值类型成员 = xx ):
- 当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
- 就是说该行为实际上会屏蔽掉对原型对象成员的访问
引用类型成员写入(实例对象,引用类型成员 = xx ):
- 同上
复杂类型成员修改(实例对象,成员 xx = xx )
- 同样会先在自己身上找到该成员,如果自己身上找到则直接修改
- 如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
- 如果一直到原型链的末端还没有找到该成员,则报错(实例对象 undefined xx = xx )
更简单的原型语法
前面在原型对象每添加一个属性和方法就要书写一遍 Person.prototype。
为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,将 Person.prototype 重置到一个新的对象。
注意:原型对象会丢失 constructor 成员,所以需要手动将 constructor 指向正确的构造函数。
Person.prototype = {
constructor : Person, // 需要手动将 constructor 指向正确的构造函数
type : "human",
sayName : function(){
console.log(this,name)
}
}
建议
在定义构造函数时,可以根据成员的功能不同,分别进行设置
- 私有成员(一般就是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 如果重置了 prototype 记得修正 constructor 的指向
内置构造函数的原型对象
所有函数都有 prototype 属性对象
Javascript中的内置构造函数也有 prototype 原型对象属性:
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
继承
封装的构造函数就是用来创建一类对象
继承指的是 类型和类型之间的继承
原型对象可以将自己的属性和方法继承给将来的实例对象使用
函数的 call 方法 6069
call 方法在调用函数的时候,有两个功能
1 更改函数内部的 this执行
2 调用函数执行内部代码
参数:第一个参数用来指定 this,第二个及以后,就是传的实参
构造函数的原型方法继承
- 拷贝继承 for in
- 原型继承
student.prototype = new person();
student.prototype.constructor = student;
组合继承
属性在构造函数内部继承,方法通过原型继承
function person(name,age){
this.name = name;
this.age = age;
}
person.prototype.sayHi = function(){
console.log(this.name + ":ni hao");
}
function student(name,age,score){
person.call(this,name,age);
this.score = score;
}
student.prototype = new person();
student.prototype.constructor = student;
var s1 = new student("zzj",18,100);
console.log(s1);
s1.sayHi();
函数声明和函数表达式
// 函数声明
function fun(){
console.log(1);
};
// 函数表达式 --- 将函数赋值给一个变量,可以是一个匿名函数
var fn = function (){
console.log(2);
};
函数声明与函数表达式的区别
- 函数声明必须有名字
- 函数声明会函数提升,在预解析阶段就已创建,声明前后都可以调用
- 函数表达式类似于变量赋值
- 函数表达式可以没有名字,例如匿名函数
- 函数表达式没有函数提升,在执行阶段创建,必须在表达式执行之后才可以调用
函数也是对象
- 函数本身也是一种对象,可以调用属性和方法
var fn = new Function('var a = "1";console.log(a)');
fun(2)
函数的调用和this 5715
// 1 普通函数 是通过给函数名或者变量名添加()方式执行
// 内部的 this 默认指向 window
function fun(){
console.log(1);
}
fun();
// 2 构造函数,是通过 new 关键字进行调用
// 内部的 this 指向的是将来创建的实例对象
function Person(name){
this.name = name;
}
var p1 = new Person("zs");
// 3 对象中的方法,是通过对象打点调用函数,然后加小括号
// 内部的 this 默认指向的是调用的对象自己
var o = {
sayHi : function(){
console.log("hhh")
}
}
o.sayHi();
// 4 事件函数,不需要加特殊的符号,只要事件被触发,会自动执行函数
// 事件函数的内部 this 指向的是事件源
document.onclick = function(){
console.log("hhh")
}
// 5 定时器和延时器中的函数,不需要加特殊符号,只要执行后,在规定的时间自动执行
// 默认内部的 this 指向的是 window
setInterval(function(){
console.log("hhh")
},1000)
-
函数的调用方式决定了this指向的不同:
call
- call() 方法调用一个函数,其具有一个指定的this值和分别地提供的参数(参数的列表)
- 注意:该方法的作用和apply()方法类似,只有一个区别,就是call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组
- 语法
fun.call(thisArg,arg1,arg2,...) - thisArg
在fun函数运行时指定的this值
如果指定了null或者undefined则内部this指向window - arg1,arg2
指定的参数列表‘
var o = {
0: 10,
1: 20,
2: 30
};
Array.prototype.push.call(o,40);
apply
1 功能:第一个可以指定函数的 this,第二个可以执行函数并传参
2 参数:第一个参数,传入一个指定让 this 指向的对象,第二个参数是函数的参数组成的数组
3 返回值:就是函数自己的返回值
// 定义一组数组,利用 apply 方法,可以将它拆开进行操作
var arr = [1,3,4,56,8];
// 想借用一些现在内置在 js 中的方法
// 利用 apply 方法,将数组传给 max 的第二个参数
console.log(Math.max.apply(Math,arr))
bind
1 功能:第一个可以指定函数的 this,bind 方法不能执行函数,但是可以传参
2 参数:第一个参数,传入一个指定让 this 指向的对象,第二个参数及以后,是函数参数的列表
3 返回值:返回一个新的指定了 this 的函数,也可以叫绑定函数
// 想修改的是定时器的函数内部的 this
var o = {
name: "zs",
age: 18,
s: function(){
setInterval(
function(){
console.log(this.age);
}.bind(this),1000);
}
}
高阶函数
- 函数可以作为参数
- 函数可以作为返回值
// 1 函数作为另一个函数的参数
// 定义一个函数,吃饭的函数,吃完饭之后,可以做其他的事情,看电影,聊天,看书
function eat(fn) {
console.log("吃饭");
// 接下来的事情不固定
fn();
}
eat(function () {
console.log("看电影");
})
// 2. 函数作为一个函数的返回值
// 需求:通过同一段代码实现以下效果
// 输出 100 + m
// 输出 1000 + m
// 输出 10000 + m
function outer(n){
return function inner(m){
console.log(m + n);
}
}
// 在外部执行 inner 函数
// 100 + m
var fun = outer(100);
fun(3);
// 1000 + m
var fun1 = outer(1000)
fun(3);
闭包
一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
-
函数定义时天生就能记住自己生成的作用域环境和函数自己,将它们形成一个密闭的环境,这就是闭包。不论函数以任何方式在任何地方进行调用,都会回到自己定义时的密闭环境进行执行。
观察闭包
- 从广义上来说,定义在全局的函数也是一个闭包,只是我们没办法将这样的函数拿到更外面的作用域进行调用,从而观察闭包的特点,
- 闭包是天生存在的,不需要额外的结构,但是我们为了方便观察闭包的特点,需要利用一些特殊结构将一个父函数内部的子函数拿到父函数外部进行调用,从而观察闭包的存在。
闭包的用途
- 可以在函数外部读取函数内部成员
- 让函数内成员始终存活在内存中