javaScript基础(一)
· 介绍JavaScript的基本数据类型。
字符串、数值、对象、布尔值、null、undefined六类基础数据类型。(ES6又新增了第七种类型Symbol值。)
- 字符串、数值、布尔值合称为原始类型的值
- null、undefined是特殊类型的值
- 对象又可分为三类:狭义的对象、数组、函数合成为引用数据类型
· JavaScript原型,原型链 ? 有什么特点?
原型:javascript的对象都有一个内置的prototype私有属性,这个属性指向的是另一个对象,我们称这个对象为原对象的原型。
- 特点:实现了属性的共享。比如:我们只要把操作方法定义在原型上,当需要时直接调用改方法即可,大大减少了代码冗余,减少内存资源的浪费。
原型链:原型并不是一个特殊的存在,它也是一个对象,也可以拥有属于它的原型。然后把对象的prototype属性想象成链条,就形成了一条原型链。
- 特点:原型链实现了继承。比如有a、b、c三个对象,a的prototype是b,b的prototype是c,即a可以调用c的属性。
· JavaScript有几种类型的值?你能画一下他们的内存图吗?
栈:原始数据类型(数值、字符串、布尔值)。
堆:引用数据类型(数组、函数、对象)。


总结:
· 声明变量是不同的内存分配
- 原始值:存储在栈内的简单数据段,也就是说,它们是直接存储在变量访问的位置。
- 引用值:存储在堆中的对象,也就是说,存在变量中的值是一个指针,指向存储对象的内存地址。因为引用值的值是会变的,如果放在栈中,会降低变量查寻的速度。相反,如果把对象存储在堆中,存储地址放在栈中,由于存储地址的大小是固定的,所以不会对变量性能造成影响。
· 复制变量时的不同
- 原始值:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量完全独立。
- 引用值:再将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给这个新变量,也就是说两个变量都指向了堆内存中同一个对象,它们中任何一个做出的改变都会反映在另一个身上。
·Javascript如何实现继承?
原型链继承:将父类型的实例赋值给子类型的原型,则子类型的原型可以访问父类型的属性和方法,以及父类型构造函数中赋值的属性和方法。
// 声明父类型
function SuperClass () {
this.superValue = true
}
// 为父类型添加公共方法
SuperClass.prototype.getSuperValue = function () {
return this.superValue
}
// 声明子类型
function SubClass () {
this.subValue = false
}
// 将父型类对象的实例赋值给子类型原型(继承父类)
SubClass.prototype = new SuperClass()
SubClass.prototype.getSubValue = function () {
return this.subValue
}
// 测试
const instance = new SubClass()
console.log(instance.getSuperValue()) // true
console.log(instance.getSubValue()) // false
console.log(instance instanceof SuperClass) // true
console.log(instance instanceof SubClass) // true
console.log(SubClass instanceof SuperClass) // false
console.log(SubClass.prototype instanceof SuperClass) // true
/** 由于instanceof是判断前面的对象是否是后面类(对象)的实例,所以console.log(SubClass instanceof SuperClass)打印false */
缺点:
- 子类型是通过原型prototype对父类型实例化来继承父类。但当父类型中的共有数据是引用类型时,会在子类型中被所有的实例共享,如此一来在一个子类型实例中更改从父类型继承过来的共有属性时,会影响其它子类型。
- 由于子类是通过原型prototype实例化父类继承的,所以在实例化父类的时候,不能向父类构造函数中传递参数。(如果参数会影响其它对象的实例。)
借用构造函数继承:即在子类型构造函数的内部调用父类型构造函数;因此可以通过使用apply()和call()方法可以在准备新建的对象上执行的构造函数。如下所示:
function SuperType () {
this.colors = [ 'red', 'blue', 'green' ]
}
function SubType () {
// 继承SuperType
SuperType.call(this)
}
var instance1 = new SubType()
var instance2 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green"]
并且解决了向父类构造函数传递参数问题,如下
function SuperType (name) {
this.colors = name
}
function SubType () {
// 继承SuperType,同时还传递了参数
SuperType.call(this, 'javascript')
this.name = 'sub'
}
function SubType2 () {
// 继承SuperType,同时还传递了参数
SuperType.call(this, 'javascript2')
}
var instance1 = new SubType()
var instance2 = new SubType2()
console.log(instance1.colors) // javascript
console.log(instance1.name) // sub
// instance1、instance2都继承了SuperType并且都传递了参数,但是它们两个互不影响。
缺点:
- 如果仅用借用构造函数,那么无法避免构造函数模式存在的问题-----方法都在函数中定义,那么函数复用就无从谈起了。
组合继承:指的是原型链和借用构造函数的技术结合一起。其是使用原型链的原型属性和方法的继承、借用构造函数来实现实例属性的继承。如下所示:
function SuperType (name) {
this.name = name
this.colors = ['red', 'blue']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType (name, age) {
// 继承SuperType的属性
SuperType.call(this, name)
this.age = age
}
// 继承SuperType方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
console.log(this.age)
}
var instance1 = new SubType('sub', 13)
console.log(instance1.colors) // ["red", "blue"]
console.log(instance1.name) // sub
console.log(instance1.age) // 13
instance1.sayName() // sub
instance1.sayAge() // 13
优点:
- 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof和isPrototypeof也能识别是基于组合继承创建的对象。
原型式继承:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。如下所示:
function object (o) {
function F () {}
F.prototype = o
return new F()
}
// object()对传入其中的对象执行了一次浅复制。也就是将传入的对象作为构造函数的原型。
var person = {
name: 'hello',
friends: ['1', '2', '3']
}
var sub1 = object(person)
sub1.name = 'sub1'
sub1.friends.push('sub1')
var sub2 = object(person)
sub2.name = 'sub2'
sub2.friends.push('sub2')
console.log(sub1.friends) // ["1", "2", "3", "sub1", "sub2"]
console.log(sub2.friends) // ["1", "2", "3", "sub1", "sub2"]
// Es5通过新增Object.create()规范了object()方法,这个方法接受两个参数:一个用作新对象原型的对象和一个新对象定义额外属性的对象。
// 传入一个参数的行为与object()一样
var person = {
name: 'hello',
friends: ['1', '2', '3']
}
var sub1 = Object.create(person)
sub1.name = 'sub1'
sub1.friends.push('sub1')
var sub2 = Object.create(person)
sub2.name = 'sub2'
sub2.friends.push('sub2')
console.log(sub1.friends) // ["1", "2", "3", "sub1", "sub2"]
console.log(sub2.friends) // ["1", "2", "3", "sub1", "sub2"]
// 传入第二个参数,定义额外属性,额外定义的任何属性都会覆盖原型对象上的同名属性。第二个参数与Object.defineProperties()方法的第二个参数格式相同。
var person = {
name: 'hello',
friends: ['1', '2', '3']
}
var sub1 = Object.create(person, {
name: {
value: 'sub1'
}
})
console.log(sub1.name)
但是引用类型值的属性始终都会共享相应的值,就和原型链一样。
寄生式继承:是与原型式继承紧密相关的一种思路,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象。
function object (o) {
function F () {}
F.prototype = o
return new F()
}
function createAno (o) {
var clone = object(o)
clone.sayHi = function () {
console.log('hi')
}
return clone
}
var person = {
name: 'sub',
arr: [ '1', '2', '3' ]
}
var sub = createAno(person)
sub.sayHi()
这个例子中的代码基于 person 返回了一个新对象---sub,新对象不仅拥有person的所有属性和方法,也拥有自己的sayHi方法。
缺点:
- 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。
寄生组合式继承:即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。
function object (o) {
function F () {}
F.prototype = o
return new F()
}
function inheritPrototype (subType, superType) {
var prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}
function SuperType (name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};
function SubType (name, age) {
SuperType.call(this, name)
this.age = age
}
inheritPrototype(SubType, SuperType)
SubType.prototype.sayAge = function () {
console.log(this.age);
}
组合继承是JavaScript 最常用的继承模式,但会两次调用父类型构造函数(1. SuperType.call(this, name); 2. SubType.prototype);这个例子体现在它只调用了一次 SuperType 构造函数(SuperType.call(this, name)),并且因此避免了在 SubType. prototype 上面创建不必要的、多余的属性。
·Javascript创建对象的几种方式?
构造函数和字面量可以用来创建单个对象。但因为使用一个接口创建很多对象,会产生大量的重复代码。以下几种方式可以解决这个问题。
工厂模式:是软件工程领域一种广为人知的设计模式,这种模式抽象了创建对象的过程。
function obj (name, age, job) {
var o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name)
}
return o
}
var s1 = obj('hello', 12, 'web')
var s2 = obj('word', 11, 'ui')
console.log(s1.name) // hello
console.log(s2.name) // word
缺点:
- 工厂模式虽然解决了创建多个相似对象问题,但却没有解决对象的识别问题(就是不知道到底是内置对象、宿主对象、自定义对象)。
构造函数模式:用构造函数可以创建特定类型的对象。像Object和Array这样的原生构造函数在运行时会自动出现在执行环境中。如下是使用构造函数模式的例子:
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name)
}
}
var s1 = new Person('hello', 12, 'web')
var s2 = new Person('word', 11, 'ui')
console.log(s1.name) // hello
console.log(s2.name) // word
/**
这里的函数名的首字母是大写。因为构造函数始终都应该以一个大写字母开头。
由于要创建Person的新实例,需要用到new操作符。以这种方式调用构造函数会经历以下四个步骤:
1. 创建一个新对象
2. 将构造函数中的作用域赋值给新对象(因此this指向了这个新对象)
3. 执行构造函数中的代码(为这个新对象添加属性)
4. 返回新对象
所以s1和s2分别保存着Person的一个不同的实例,但这两个对象都有一个constructor(构造函数)属性,指向的是Person,如下:
*/
console.log(s1.constructor === Person) // true
console.log(s2.constructor === Person) // true
/**
如果需要检测对象类型就可以用到instanceof操作可以验证(可以验证创建的所有对象是否是某个构造函数的实例:上面创建的对象都是Person、Object的实例)
*/
缺点:
- 使用构造函数的主要问题,就是每一个方法都要在每个实例上重新创建一次。
原型模式:创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象。prototype其实就是通过调用构造函数而创建的那个实例对象的原型对象。
function Person () {}
Person.prototype.name = 'hello'
Person.prototype.age = 12
Person.prototype.job = 'web'
Person.prototype.sayName = function () {
console.log(this.name)
}
var p1 = new Person()
var p2 = new Person()
p1.sayName() // hello
p2.sayName() // hello
/**
所有的方法和属性直接添加到Person的prototype属性中。新对象的这些属性和方法是有所有的实例共享的。
1. 理解原生对象:只要创建了一个新的构造函数,就会根据一组特点的规则为该函数创建一个prototype属性,这个属性指向的是这个函数的原型对象。在默认情况下原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指针,指向prototype所在的函数,如上例:Person.prototype.constructor指向的是Person。
a. isPrototypeOf方法
创建了构造函数之后,调用构造函数创建一个新实例后,该实例包含一个__proto__(指的是:创建这个实例的构造函数的原型)属性,如上例:p1.__proto__ === Person.prototype。
然而可以使用isPrototypeOf()方法来判断对象直接是否存在这种关系。
*/
console.log(Person.prototype.isPrototype(p1)) // true
console.log(Person.prototype.isPrototype(p2)) // true
/**
b. Object.getPrototypeOf方法:ES5新增的方法,返回一个对象。这个对象就是创建这个实例对象的构造函数的原型对象。
*/
console.log(Object.getPrototypeOf(p1) === Person.prototype)
// 还可以获取原型对象上的属性
console.log(Object.getPrototypeOf(p1).name)
/**
2. 原型与in操作符:有两种方式使用in操作符;一种的单独使用,另一种是for-in。单独使用时,通过in操作符可以知道给定的属性是否能在对象上访问(只要能访问就行,不论是实例还是原型上)。然后可以通过hasOwnproperty方法来判断属性是否是在实例上。用上例:
*/
console.log(p1.hasOwnProperty('name')) // false
console.log('name' in p1) // true
/**
p1.name = 'lll'
console.log(p1.hasOwnProperty('name')) // true
console.log('name' in p1) // true
在使用for-in时,返回的是所有能够通过对象访问、可枚举的属性,包括实例中的、原型中的。但是IE8及以下版本中例外,开发人员自定义的属性有可能是不可枚举的。如下:
var o = {
toString: function () {
return 'on my o'
}
}
for (var key in o) {
if (key === 'toString') {
console.log('toStrinig') // 在IE8及以下版本会不显示
}
}
*/
// 正常的例子,如下:
for (var inKey in p1) {
console.log(inKey) // "name", "age", "job", "sayName"
}
/**
a. 可以使用Object.keys()来获取对象的属性,不包括不可枚举和原型对象上的属性,如下
*/
var p1Keys = Object.keys(p1)
var keys = Object.keys(Person.prototype)
console.log(p1Keys) // []
console.log(keys) // ["name", "age", "job", "sayName"]
/**
b. 使用Object.getOwnpropertyNames()方法来获取对象的属性,不包括原型对象上的属性但是包括不可枚举属性,如下:
*/
var ownKeys = Object.getOwnPropertyNames(Person.prototype)
var p1OwnKeys = Object.getOwnPropertyNames(p1)
console.log(ownKeys) // ["constructor", "name", "age", "job", "sayName"]
console.log(p1OwnKeys) // []
/** 3. 原型模式的缺点:如果原型包含引用类型的属性,会引起其共享所导致的问题。 */
组合使用构造函数模式和原型模式:构造函数用于定义实例属性,原型属性用于定义方法和共享的属性。结果每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用。
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.arr = ['a', 'b', 'c']
}
Person.prototype.sayName = function () {
console.log(this.name)
}
var p1 = new Person('hello', 12, 'web')
var p2 = new Person('work', 11, 'ui')
p1.arr.push('d')
console.log(p1.arr)
console.log(p2.arr)
console.log(p1.arr === p2.arr)
console.log(p1.sayName === p2.sayName)
这种构造函数和原型混成的模式,是目前ECMAScrpt中使用最广泛、认同度最高的一种创建自定义类型的方法。
动态原型模式:可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。如下:
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
if (typeof this.sayName != 'function') {
Person.prototype.sayName = function () {
console.log(name)
}
}
}
var p1 = new Person('hello', 12, 'web')
p1.sayName() // hello
/** 只有在sayName()不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用 */
寄生构造函数模式:这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。这个函数又很想典型的构造函数。如下例:
function Person(name, age, job) {
var o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name)
}
return o
}
var p1 = new Person('hello', 12, 'web')
p1.sayName() // hello
/** 这里除了使用new操作符并把使用的包装函数叫构造函数之外,这个模式跟工厂模式其实一模一样。 */
/** 这个模式可以在特殊情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用如下模式: */
function Sper() {
// 创建数组
var arr = new Array()
// 添加值
arr.push.apply(arr, arguments)
// 添加方法
arr.toSper = function () {
return this.json('|')
}
return arr
}
var p2 = new Sper('red', 'blue', 'green')
console.log(p2.toSper())