夯实基础-数据类型与继承

数据类型是基础中的基础,你们每天遇到,咱们此次来讨论深一点,将咱们认为理所固然的事情背后的原理发掘;继承也是前端基础一个大考点,看看继承的原理与使用场景。javascript

本文讨论如下几个点:html

  1. JavaScript数据类型
  2. 不一样数据类型对应的数据结构
  3. 数据类型转换
  4. 数组与对象的api表
  5. new关键字背后干了些什么
  6. 原型与原型属性与构造函数区别于联系
  7. 实例化,混入,继承,多态是什么意思

数据类型

最新的 ECMAScript 标准定义了 7 种数据类型:前端

这个分类咱们应该是至关熟悉了,当时这是按照什么标准分类的。git

数据类型对应的数据结构

事实上上面的分类标准是按照不一样数据在计算机内存中的结构分类的。咱们都知道JavaScript中的变量运行的时候是存在内存中的,若是接触过java的人应该知道,内存中也分为栈内存和堆内存。github

栈(stack)

基本类型Undefined、Null、Boolean、Number 和String。这些类型在内存中分别占有固定大小的空间,他们的值保存在栈内存,他们的值保存在栈内存,咱们经过按值来访问的。面试

var a = '1';
var b = '1';
a === b;

上述代码执行时候,能够理解为:编程

  1. 声明变量a,b,为a,b分配一个栈内存空间。(变量提高)
  2. 要赋值a,将a的字面量'1'做为值存储到a在栈内存的值中。
  3. 要赋值b,一样将'1'做为栈内存的值存储。
  4. 这种简单数据类型,都是在栈内存中保存其值。

JavaScript中的原始值(基本数据类型)均可预知其最大最小内存大小,因此建立的时候直接分配对应的内存空间。segmentfault

堆(heap)

复杂的数据类型,如object,array,function等,没法提早预知其要占用多少内存空间,因此这个数据类型被放入了堆内存中,同时在栈内存中保存其堆内存的地址,访问这些变量的时候,在栈内存中获取到其内存地址,而后访问到该对象,这种方式叫按引用访问api

var a = 'hello world';
var b = 123;
var c = null;
var d = undefined;
var e = {};
var f = function(){console.log(1);};
var g = [1,2,a];

其在内存中的简易模型以下:

clipboard.png

上面这个图并非彻底准确的,这里只是简单形容一下不一样数据类型变量的存储关系,偏底层的知识真的须要单独开一篇来说了。

经过上面的图我想应该一目了然了,基本数据类型都是存在栈内存中的,复杂对象则是存在堆内存中,栈内存变量保存的是其内存地址。这也应该想到了咱们常常遇到的问题:对象之间赋值,赋值的是真正的内存地址;对象相互比较===,比较的是内存地址。

变量赋值

JavaScript 引用指向的是值。若是一个值有 10 个引用,这些引用指向的都是同一个值,它们相互之间没有引用 / 指向关系。

JavaScript 对值和引用的赋值 / 传递在语法上没有区别,彻底根据值的类型来决定:

  1. 简单数据类型老是经过值复制的方式来赋值 / 传递。
  2. 复杂数据类型则老是经过引用复制的方式来赋值 / 传递。

包装类和类型转换

内置对象

JavaScript内置了一些对象,这些对象能够在全局任意地方调用,而且有各自的属性和方法。MDN上罗列了所有,这里只挑一部分对象说明:

  • Object
  • Array
  • Function
  • String
  • Number
  • Boolean
  • Math
  • Date
  • RegExp

ok经过上面的几个内置对象就会发现一些问题:一些基本数据类型(String,Number,Boolean)有对应的内置对象,可是其余的一些(Null, Undefined)就没有,复杂数据类型则都有,这是为何。

包装类

var a = 'hello world';
a[1]; // 'e'
a.length; // 11
a.toString(); // hello world
a.valueOf(); // hello world
a.split(' '); // ['hello', 'world']

有没有想过,变量a命名是个基本类型,不是对象,为何会有这么多属性和方法。由于这些内置的属性和方法都在内置对象String上。

事实上当你调用这些基本数据类型上属性和方法时候,引擎会自动寻找其是否有对应的包装类,有的话生成一个包装类的实例供你使用(使用以后销毁),不然报错。

var a = 'hello world';
a.customAttribute // undefined
String.prototype.customAttribute = 'custom';
var b = 'hello world';
b.customAttribute // custom

咱们如今想要访问属性customAttribute,这个属性没有在内置对象上,因此获取到的值是undefined;咱们向内置对象的原型链上添加该属性,以后全部的string上均可以获取到该值。

类型转换

JavaScript中的类型转换也是个大坑,很多面试都会问到。JavaScript 是一种动态类型语言,变量没有类型限制,能够随时赋予任意值。

显示转换

直接调用对应的包装类进行转换。具体可分红三种状况:

// 数值:转换后仍是原来的值
Number(324) // 324

// 字符串:若是能够被解析为数值,则转换为相应的数值
Number('324') // 324

// 字符串:若是不能够被解析为数值,返回 NaN
Number('324abc') // NaN

// 空字符串转为0
Number('') // 0

// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0

// undefined:转成 NaN
Number(undefined) // NaN

// null:转成0
Number(null) // 0

使用Number包装类来进行类型转换,隐藏的逻辑:

  1. 调用对象自身的valueOf方法。若是返回原始类型的值,则直接对该值使用Number函数,再也不进行后续步骤。
  2. 若是valueOf方法返回的仍是对象,则改成调用对象自身的toString方法。若是toString方法返回原始类型的值,则对该值使用Number函数,再也不进行后续步骤。
  3. 若是toString方法返回的是对象,就报错。
var obj = {x: 1};
Number(obj) // NaN

// 等同于
if (typeof obj.valueOf() === 'object') {
  Number(obj.toString());
} else {
  Number(obj.valueOf());
}

var obj1 = {
  valueOf: function () {
    return {};
  },
  toString: function () {
    return {};
  }
};

Number(obj1)
// TypeError: Cannot convert object to primitive value

Number({
  valueOf: function () {
    return 2;
  }
})
// 2

Number({
  toString: function () {
    return 3;
  }
})
// 3

Number({
  valueOf: function () {
    return 2;
  },
  toString: function () {
    return 3;
  }
})
// 2

若是使用String则规则相对简单:

  1. 值为基本数据类型

    • 数值:转为相应的字符串。
    • 字符串:转换后仍是原来的值。
    • 布尔值true转为字符串"true"false转为字符串"false"
    • undefined:转为字符串"undefined"
    • null:转为字符串"null"
  2. 值为对象

    1. 先调用对象自身的toString方法。若是返回原始类型的值,则对该值使用String函数,再也不进行如下步骤。
    2. 若是toString方法返回的是对象,再调用原对象的valueOf方法。若是valueOf方法返回原始类型的值,则对该值使用String函数,再也不进行如下步骤。
    3. 若是valueOf方法返回的是对象,就报错。

Boolean规则更简单:除了五个值(undefined,null,(+/-)0,NaN,‘’)的转换结果为false,其余的值所有为true

隐式转换

隐式转换也分三种状况:

转布尔值

JavaScript 遇到预期为布尔值的地方(好比 if语句的条件部分),就会将非布尔值的参数自动转换为布值。系统内部会自动调用 Boolean函数。

因此跟上面同样,所以除了五个值(undefined,null,(+/-)0,NaN,‘’),其余都是自动转为true

转字符串

JavaScript 遇到预期为字符串的地方,就会将非字符串的值自动转为字符串。具体规则是,先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串。

字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另外一个值为非字符串,则后者转为字符串。

'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"

转数值

JavaScript 遇到预期为数值的地方,就会将参数值自动转换为数值。系统内部会自动调用 Number函数。

除了加法运算符(+)有可能把运算子转为字符串,其余运算符都会把运算子自动转成数值。

'5' - '2' // 3
'5' * '2' // 10
true - 1  // 0
false - 1 // -1
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN

具体参考阮一峰老师:JavaScript类型转换

数组和对象

这三个复杂对象咱们太熟悉不过了,天天都在打交道。可是实际上咱们也并非彻底掌握。

数组(Array)

数组方法不少,咱们能够分类来整理记忆。

有哪些方法返回的是新数组

  1. concat
  2. slice
  3. filter
  4. map
  5. forEach

遍历数组方法有几种,区别在于什么

常见的有:

  1. map:返回新数组,数组的每一项都是测试函数的返回值。
  2. forEach:不返回任何值,只是单纯遍历一遍数组。
  3. every:遍历数组全部元素,直到测试函数返回第一个false中止。
  4. some:遍历数组全部元素,直到测试函数返回第一个true中止。
  5. for循环:写起来最麻烦,可是性能最好。

filter方法传入函数的参数有几个,都是什么含义

不仅是filter方法,相似这种第一个参数为callback的方法如:some,every,forEach,map,find,findIndex的方法callback参数都同样:currentValue,Index,array。
github上参照了MDN整理了一份完整的文档,用于本身的查缺补漏。

对象(Object)

建立对象的方法有几种

  1. 字面量方式:

    var person={
        name:"SF",
        age:25
        say:function(){
           alert(this.name+"今年"+this.age);
        }
    };
    person.say();
  2. 利用Object对象建立实例

    var my = new Object();
    my.name = "SF"; //JavaScript的发明者
    my.age = 25;
    my.say = function() { 
      alert("我是"+this.name+"今年"+my.age);
    }
    my.say();
    
    var obj = Object.create(null);
    obj.name = 'SF';
  3. 构造函数

    function Person(name,age) { 
      this.name = name; 
      this.age = age; 
      this.say = function() { 
          alert("我叫" + this.name + ",今年" + this.age + "岁); 
      }
    }
    var my = new Person("SF",25); //实例化、建立对象
    my.say(); //调用say()方法
  4. 原型模式

    function Person() {
    }
    Person.prototype.name = 'aus';
    Person.prototype.job = 'fe'
    Person.prototype.sayName = function() {
      console.log(this.name)
    }
    var person1 = new Person();
  5. 组合构造函数和原型

    function Person( name, age, job ) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.friends = ["Shelby","Court"];
    }
    
    Person.prototype = {
        constructor: Person,
        sayName: function(){
            alert(this.name);
        }
    }
    
    var person1 = new Person("Nicholas", 29, "software Engineer");
    var person2 = new Person("Greg", 27, "Doctor");
    
    person1.friends.push("Van");
    
    alert(person1.friends); //"Shelby,Court,Van"
    alert(person2.friends); //"Shelby,Court"
    alert(person1.friends === person2.friends); //false
    alert(person1.sayName === person2.sayName); //true

对象的扩展密封和冻结有什么区别

  • 扩展特性

    • Object.isExtensible 方法
    • Object.preventExtensions 方法
  • 密封特性

    • Object.isSealed 方法
    • Object.seal 方法
  • 冻结特性

    • Object.isFrozen 方法
    • Object.freeze 方法

      • 浅冻结深冻结

简单说就是对象有可扩展性(能够随意添加属性),限制对象的可扩展性(Object.preventExtensions)以后,对象不可添加新属性(可是现有属性能够修改和删除)。

密封对象(seal)指的是对象的属性不可增长或者删除,而且属性配置不可修改(属性值可修改)。

冻结对象(freeze)则更加严格,不可增长或者删除属性,而且属性彻底不可修改。

这里不作过多介绍,详细能够看这里

怎样快速实现浅拷贝以及深拷贝

Object.assign是常见的浅拷贝方法,怎样本身实现。

// 利用原生api
function shallowClone(obj) {
  return Object.create(
      Object.getPrototypeOf(obj), 
      Object.getOwnPropertyDescriptors(obj) 
  );
}

// 属性浅拷贝
function shallowCopy(copyObj) {
  var obj = {};
  for ( var i in copyObj) {
    obj[i] = copyObj[i];
  }
  return obj;
}

深拷贝以前整理过:github

对象的方法参照MDN整理了一份,github

原型链

这节算是给继承铺垫基础知识了,js里最出名的原型和原型链,面试必考,平常开发也特别常见。

prototype

prototype中文译为'原型',大部分Object和Function都有prototype。我的以为原型是一个特殊的普通对象,对象里面的属性和方法都用于指定的用途:共享。咱们能够按照本身的意愿去修改原型,而且从新被共享。

当建立函数的时候,每个函数都会自动有一个prototype属性,这个属性的值是空对象(空对象不是空)。

一旦你把这个函数当成构造函数调用(经过new调用)JS会建立构造函数的实例,实例是不具备原型的。

function A (){};
A.prototype // {}

var a = new A();
a.prototype // undefined

proto

中文翻译过来叫'原型属性',这是一个隐式属性,不可被枚举,可是他的用途相当重要。每一个对象建立的时候,都会有一个隐式的属性__proto__,该属性的值是其对应的原型(其实就是说明 该对象的来源)。

function A (){};
A.__proto__ === Function.prototype; // true

var b = {};
b.__proto__ === Object.prototype; // true

var c = [];
c.__proto__ === Array.prototype; // true

能够肯定的是,__proto__指向的是其构造函数的原型

contructor

构造函数实例都拥有指向其构造函数的constructor属性。constructor属性的值是一个函数对象 为了将实例的构造器的原型对象暴露出来。

function A(){};
A.constructor === Function // true

var a = new A();
a.construtor === A // true

var obj = {};
obj.constructor === Object // true

能够肯定的是,constructor属性指向其构造函数

关系

上面三者的关系能够用下图表示:
clipboard.png
这里就不得不提一句:使用new关键字实例化对象,内在过程到底发生了什么。

咱们能够理解为将new关键字实例化对象拆成两步:

function A(){};

function create (base) {
    var obj  = {};
    obj.__proto__ = base.prototype;
    base.call(obj);
    return obj;
}

var a = create(A);

a instanceof A // true

原型链

上面三个角色到期了以后,就到了另外一个重点:原型链。

var a = Object.create(null);
a.a = 1;
var b = Object.create(a);
b.b = 2;
var c = Object.create(b);
c.c = 3;

c.a // 1
c.b // 2
c.c // 3

a.d = 4;
c.d;

c.a = 0;
c.a; // 0

上面这个例子用到了Object.create函数建立了一个原型为空的对象a。能够看到c并无a,b属性,可是却能够读出该值来,这就是原型链。

当访问一个对象的属性(方法)的时候,若是对象自身没有该属性(方法),就会去该对象的__proto__上寻找,若是__proto__上也没有,就去__proto__.__proto__上寻找,以此类推,直到找到一个值返回;若没有则返回undefined。这种按照对象原型属性寻找造成一个相似链状的结构,叫作原型链。
clipboard.png
画个图表示:
clipboard.png
上图中的__proto__红线能够理解为原型链

这里要注意的是,对象的原型属性,保存的是对象的内存地址引用,须要读取原型属性的时候会找到该对象当时的状态,因此更改原型链上原型属性对象,会对该条原型链上的其余对象形成影响。

继承

ok通过这么多铺垫终于来到了继承,继承是面向对象里面最重要的概念之一。咱们先来把相关概念介绍,再来看动手实现。

无论是实例,混入或者继承,他们的诞生都是为了解决同一个问题:代码复用。只不过实现方式不一样。

实例

这个是咱们平常开发中最经常使用的一种。

var date = new Date();

var instanceLightBox = new LightBox();

实例化一个对象能够理解为调用类的构造函数,返回一个拥有类全部属性和方法的对象。

这样说可能也不许确,咱们以var a = new A();为例,实例化一个对象有几个特色:

  1. a是一个object;
  2. a的构造函数是A;
  3. A构造函数中的非私有属性会被a获取到;
  4. A的原型是a的原型属性;
function A () {
    this.a = 1;
};

A.prototype.getA = function(){
    return this.a;
}

var a = new A();

a.a; // 1
a.getA(); // 1

事实上咱们在上面已经讲解了调用new关键字发生了什么,这里原理很少讲。为何要用实例化类:咱们能够吧构造函数当作一个工厂,工厂产出了定制化模板(构造函数)和标准模板(构造函数的原型)的产品;咱们能够经过屡次实例化一个类,产出多个同样的产品,从而实现了代码复用。

混入(mixin)

混入更像是一个加工厂,对已有的对象进行添加新属性的操做。

function A (){
    this.a = 1;
};

// 一个很是简单的mixin例子
function mixin(sourceObj, targetObj){
    for (var key in sourceObj) {
        // 只会在不存在的状况下复制
        if (!(key in targetObj)) {
            targetObj[key] = sourceObj[key];
        }
    }
}

var a = new A();
var b = {b:2};
mixin(b, a);
a.b; // 2

这个例子能够看到,targetObj混入了sourceObj的特有属性,若是属性是方法或者对象的话,targetObj保存的知识对象的引用,而不是本身独有的属性,这样sourceObject更改targetObj也会跟着更改。

继承(extend)

继承里面有两个角色,父类和子类。继承理解为获得父类全部的属性,而且能够重写这些属性。一样是得到一个function所有的属性和方法,我认为实例和继承的最大区别在于实例是构造函数实例对象,继承是类继承类,数据类型有明显区别。

咱们先来看看ES6中的继承:

class Parent {
    constructor (props) {
        const {name, phone} = props;
        this.name = name;
        this.phone = phone;
    }
       getInfo(){
        return this.name + ':' + this.phone;
    }
}

class Child extends Parent {
    constructor(props){
        super(props);
        const {gender} = props;
        this.gender = gender;
    }
    getNewInfo(){
        return this.name + ':' + this.gender + ':' + this.phone;
    }    
}

var childIns = new Child({
    name: 'aus',
    gender: 'male',
    phone: '1888888888'
});

先不讨论继承是如何实现的,先来看看继承的结果。ES6中的继承,Child类拿到了Parent类的构造器里的非属性和原型上的全部属性,而且能够扩展本身的私有属性和原型属性。可是父类和子类仍然公用父类的原型。

继承有三个特色:

  1. 子类拥有父类非私有的属性和方法。
  2. 子类能够拥有本身属性和方法,即子类能够对父类进行扩展。
  3. 子类能够用本身的方式实现父类的方法。

多态

这里多态不详细介绍,咱们来了解概念与实例。

多态:同一操做做用于不一样的对象,能够有不一样的解释,产生不一样的执行结果。

举个例子,父类原型上有个方法a,子类原型上有个同名方法a,这样在子类实例上调用a方法必然是子类定义的a,可是我若是想用父类上的a怎么办。

class Parent {
    constructor (props) {
        const {name, phone} = props;
        this.name = name;
        this.phone = phone;
    }
       getInfo(){
        return this.name + ':' + this.phone;
    }
}

class Child extends Parent {
    constructor(props){
        super(props);
        const {gender} = props;
        this.gender = gender;
    }
    getInfo(from){
        // 全完自定义
        if('child' === from){
            return this.getNewInfo();
        } else {
            return super.getInfo();   
        }
    }
    getNewInfo(){
        return this.name + ':' + this.gender + ':' + this.phone;
    }    
}

var childIns = new Child({
    name: 'aus',
    gender: 'male',
    phone: '1888888888'
});
多态是一个很是普遍的话题,咱们如今所说的“相对”只是多态的一个方面:任何方法均可以引用继承层次中高层的方法(不管高层的方法名和当前方法名是否相同)。之因此说“相对”是由于咱们并不会定义想要访问的绝对继承层次(或者说类),而是使用相对引用“查找上一层”。

继承实现

一道很是常见的面试题,有多种方法,分红两个思路,篇幅有限,不过多介绍,详细的文档在github上,或者自行google。

参考

  1. 《JavaScript权威指南》
  2. 《JavaScript高级程序设计》
  3. 《你所不知道的JavaScript》
  4. JavaScript变量——栈内存or堆内存
  5. 内存管理
  6. 数据类型转换
  7. 面向对象编程三大特性------封装、继承、多态
相关文章
相关标签/搜索