数据的作用域
- JS有两种作用域:全局作用域和函数作用域
- 内部的作用域能访问外部,反之不行。访问时从内向外依次查找。
- 如果在内部的作用域中访问了外部,则会产生闭包。
- 内部作用域能访问的外部,取决于函数定义的位置,和调用无关
- 作用域内定义的变量、函数声明会提升到作用域顶部
全局对象
无论是浏览器环境,还是node环境,都会提供一个全局对象
- 浏览器环境:window
- node环境:global
全局对象有下面几个特点:
-
全局对象的属性可以被直接访问
-
给未声明的变量赋值,实际就是给全局对象的属性赋值
-
所有的全局变量、全局函数都会附加到全局对象
原型
原型要解决的问题
上图中,通过构造函数可以创建一个用户对象
这种做法有一个严重的缺陷,就是每个用户对象中都拥有一个sayHi
方法,对于每个用户而言,sayHi
方法是完全一样的,没必要为每个用户单独生成一个。
要解决这个问题,必须学习原型
原型是如何解决的
-
原型
每个函数都会自动附带一个属性
prototype
,这个属性的值是一个普通对象,称之为原型对象 -
实例
instance,通过
new
产生的对象称之为实例。 -
隐式原型
每个实例都拥有一个特殊的属性
__proto__
,称之为隐式原型,它指向构造函数的原型
这一切有何意义?
当访问实例成员时,先找自身,如果不存在,会自动从隐式原型中寻找
这样一来,我们可以把那些公共成员,放到函数的原型中,即可被所有实例共享
this
不同的场景,这 指代的含义不同,JS中的this关键字也是如此:
-
在全局代码中使用this,指代全局对象
-
在函数中使用this,它的指向完全取决于函数是如何被调用的
调用方式 示例 函数中的this指向 通过new调用 new method()
新对象 直接调用 method()
全局对象 通过对象调用 obj.method()
前面的对象 call method.call(ctx)
call的第一个参数 apply method.apply(ctx)
apply的第一个参数
原型链
什么是原型链
所有的对象都是通过new 函数
的方式创建的
var u1 = new User('邓', '旭明'); // 对象 u1 通过 new User 创建
var u2 = { // 对象 u2 通过 new Object 创建
firstName: '莫',
lastName: '妮卡'
}
// 等效于
var u2 = new Object();
u2.firstName = '莫';
u2.lastName = '妮卡';
上面的代码形成的原型图如下
原型对象本身也是一个对象,默认情况下,是通过new Object
创建的,因此,上面的两幅原型图是可以发生关联的
可以看出,u1的隐式原型形成了一个链条,称之为原型链
当读取对象成员时,会先看对象自身是否有该成员,如果没有,就依次在其原型链上查找
完整的链条
对开发的影响
在原型上更改会产生多大影响
更改构造函数的原型会对所有原型链上有该构造函数的原型的对象产生影响
学会利用原型链判断类型
-
instanceof
关键字【常用】object instanceof constructor // 判断object的原型链中,是否存在constructor的原型
-
Object.getPrototypeOf()
【不常用】Object.getPrototypeOf(object); // 返回object的隐式原型
学会创建空原型的对象
-
利用
Object.create()
Object.create(target); // 返回一个新对象,新对象以target作为隐式原型
-
利用
Object.setPrototypeOf()
Object.setPrototypeOf(obj, prototype); // 设置obj的隐式原型为prototype
继承
会员系统
视频网站有两种会员:
- 普通会员
- 属性:用户名、密码
- 方法:观看免费视频
- VIP会员
- 属性:普通会员的所有属性、会员到期时间
- 方法:普通会员的所有方法、观看付费视频
如果我们需要使用构造函数来创建会员,如何书写构造函数才能实现上面的需求?
// 普通会员的构造函数
function User(username, password){
this.username = username;
this.password = password;
}
User.prototype.playFreeVideo = function(){
console.log('观看免费视频')
}
// VIP会员的构造函数
function VIPUser(username, password, expires){
this.username = username;
this.password = password;
this.expires = expires;
}
VIPUser.prototype.playFreeVideo = function(){
console.log('观看免费视频')
}
VIPUser.prototype.playPayVideo = function(){
console.log('观看付费视频')
}
上面的代码出现了两处重复代码:
-
VIPUser的构造函数中包含重复代码
this.username = username; this.password = password;
这段代码和User构造函数并没有区别,可以想象得到,将来也不会有区别,即:普通用户该有的属性,VIP用户一定有
-
VIPUser的原型上包含了重复代码
VIPUser.prototype.playFreeVideo = function(){ console.log('观看免费视频') }
这个方法和User上的同名方法逻辑完全一致,可以想象得到,将来也不会有区别,即:普通用户该有的方法,VIP用户一定有
处理构造器内部的重复
可以将VIPUser构造器改写为
function VIPUser(username, password, expires){
User.call(this, username, password);
this.expires = expires;
}
处理原型上的重复
只需要将原型链设置为下面的结构即可
仅需一句代码即可
Object.setPrototypeOf(VIPUser.prototype, User.prototype)
至此,完美的解决了之前提到的两处重复代码的问题
这和继承有什么关系
继承是面向对象的概念,它描述了两个对象类型(类,构造函数)之间的关系
如果在逻辑上可以描述为:A不一定是B,但B一定是A,则:B继承A、A派生B、A是B的父类、B是A的子类
子类的实例应该自动拥有父类的所有成员
继承具有两个特性:
- 单根性:子类最多只有一个父类
- 传递性:间接父类的成员会传递到子类中
如何在JS中封装继承
function inherit(Child, Parent){
// 在原型链上完成继承
Object.setPrototypeOf(Child.prototype, Parent.prototype);
}