JS对象需要分类吗?

描述抽象的事物需要分类

JS 需要计算存储抽象的概念

从实例看JS对象分类的必要性

一个简单地例子:输出各种形状的面积和周长

先从正方形开始

1
2
3
4
5
6
7
8
9
let square = {
    width: 5,
    getArea(){
        return this.width * this.width
    },
    getLength(){
        return this.width * 4
    }
}

分析正方形有三个属性:边长、面积、周长

来一打

用循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let squareList = []
for(let i = 0; i < 12; i++){
    squareList[i] = {
        width: 5,
        getArea(){
            return this.width * this.width
        },
        getLength(){
            return this.width * 4
        }
    }
}
squareList[0].getArea === squareList[1].getArea //false

width 要不同

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
let squareList = []
let widthList = [5,6,5,6,5,6,5,6,5,6,5,6]
for(let i = 0; i < 12; i++){
    squareList[i] = {
        width: widthList[i],
        getArea(){
            return this.width * this.width
        },
        getLength(){
            return this.width * 4
        }
    }
}
squareList[0].getArea === squareList[1].getArea //false

重复,浪费太多内存,画内存图

repeatCode.jpg

内存图,重复的函数

借助原型,将12个对象的共用属性放到原型里

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let squareList = []
let widthList = [5,6,5,6,5,6,5,6,5,6,5,6]
let squarePrototype = { //设置原型
    getArea(){
        return this.width * this.width
    },
    getLength(){
        return this.width * 4
    }
}
for(let i = 0; i < 12; i++){ //循环创建12个对象
    squareList[i] = Object.create(squarePrototype) //创建原型指向
    squareList[i].width = widthList[i] //添加对应宽度
}
squareList[0].__proto__ === squareList[1].__proto__ //true
squareList[0].getArea === squareList[1].getArea //true

repeatCode2.jpg

代码太分散

把代码抽到一个函数里,然后调用函数

抽离到函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let squareList = []
let widthList = [5,6,5,6,5,6,5,6,5,6,5,6]

==========

++++++++++
function createSquare(width){ //用构造函数作为模板
    let obj = Object.create(squarePrototype) //以squarePrototype为原型创建空对象
    obj.width = width
    return obj
} //封装
==========
let squarePrototype = {
    getArea(){
        return this.width * this.width
    },
    getLength(){
        return this.width * 4
    }
}
for(let i = 0; i < 12; i++){
===========

+++++++++++
    squareList[i]  = createSquare(widthList[i])
    // 1)将不变的属性放入原型。
    // 2)用此原型创建空对象,写到构造函数里。
    // 3)将不同的属性作为形参传入构造函数。
    // 4)循环创建square。
}

squarePrototype原型和createSquare函数还是分散的

合并,把原型放到函数上

函数和原型结合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
let squareList = []
let widthList = [5,6,5,6,5,6,5,6,5,6,5,6]
function createSquare(width){
==========
++++++++++
    let obj = Object.create(createSquare.squarePrototype) //以createSquare.squarePrototype为原型创建空对象
    //执行时机,只有调用函数createSquare的时候,才执行 let obj = Object.create(createSquare.squarePrototype)
    //先定义,并未执行 
    //只要在调用createSquare()前,定义好createSquare.squarePrototype
==========
    obj.width = width
    return obj
}
==========
++++++++++
createSquare.squarePrototype = { //把原型放到函数createSquare的属性上,并把createSquare.squarePrototype作为构造函数的参数,创建原型
==========
    getArea(){
        return this.width * this.width
    },
    getLength(){
        return this.width * 4
    },
==========
++++++++++
    constructor: createSquare //方便通过原型找到构造函数,互相引用
}
==========
for(let i = 0; i < 12; i++){
    squareList[i] = createSquare(widthList[i])
    console,log(squareList[i].constructor)//打印构造这个对象的函数
}

原型放到构造函数上,把构造函数放到constructor

拿到函数,方便得找到原型;通过原型,找到函数,互相引用

让代码固定下来,直接使用

new操作符

函数和原型结合(重写)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let squareList = []
let widthList = [5,6,5,6,5,6,5,6,5,6,5,6]
function Square(width){ //首字母大写
    this.width = width
}
Square.prototype.getArea = function(){
    return this.width * this.width
}
Square.prototype.getLength = function(){
    return this * 4
}
for (let i = 0; i < 12; i++){
    squareList[i] = new Square(widthList[i])
    console.log(squareList[i].constructor)
}

Square.prototypenew Square

没有一句多余

每个函数都有prototype属性,是有B.E.意为之

每个prototype都有constructor亦然

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function f1(){}
console.dir(f1)

< ƒ f1()
arguments: null
caller: null
length: 0
name: "f1"
prototype: {constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: VM45:1
[[Scopes]]: Scopes[2]

自带BGM:prototype constructor(-> fn)

1
2
function f1(){}
f1.prototype.constructor === f1 //true

对比

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
==================
++++++++++++++++++
function createSquare(width){
    let obj = Object.create(createSquare.squarePrototype)
    obj.width = width
    return obj
}
------------------
function Square(width){
    this.width = width
}
==================
++++++++++++++++++
createSquare.squarePrototype = {
    getArea(){
        return this.width * this.width
    },
    getLength(){
        return this.width * 4
    },
    constructor: createSquare
}
------------------
Square.prototype.getArea = function(){
    return this.width * this.width
}
Square.prototype.getLength = function(){
    return this.width * 4
}
==================
++++++++++++++++++
let square = createSquare(5)
------------------
let square = new Square(6)
==================

函数大小写

上面的代码(+++++)被简化为下面的代码(—–)惟一的区别是要用new来调用

new节省了let obj = Object.create(createSquare.squarePrototype)return obj

this.width = width代替obj.width = width

.getArea等共有属性,必须一个个添加到构造函数的prototype上,不能用赋值操作符,因为每个函数自带prototype.constructor等属性,不可以被覆盖掉

除非用Object.assign()

小结

new X()自动做了四件事

  • 自动创建空对象
  • 自动为空对象关联原型,原型地址为X.prototype
  • 自动将空对象作为this关键字运行构造函数
  • 自动return this

用了new,你只需

  • 声明构造函数时,添加自身属性
  • 之后在声明的构造函数的原型上添加共有属性
  • new一个构造函数,来声明函数实例

构造函数X

  • X函数本身用来给对象本身添加属性
  • X.prototype对象用来保存对象的共用属性

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    function Dog(name,color,type){
    this.name = name
    this.color = color
    this.type = type
    }
    Dog.prototype.bark = function(){console.log('汪汪')}
    let dog1 = new Dog('小白','白','白熊')
    console.dir(dog1)
    dog1.bark()
    Dog.prototype.x = '狗子' //共用属性的一般是函数,但其他也可
    let dog2 = new Dog()
    dog2.x
    

this目前就片面地理解为指代那个还没被定义的实例化对象

题外话

大小写

  • 所有构造函数(专门用于创建对象的函数)首字母大写
  • 所有被构造出来的对象,首字母小写

词性

  • new后面的函数,使用名词形式,如new Person()new Object
  • 其他函数,一般使用动词打次开头,如createSquare(5)createElement('div')
  • 还有其他…

总结一个重要公式

JS里唯一的一个公式

确定一个对象的原型的方法

为什么

  • let obj = new Object()的原型是Object.prototype
  • let arr = new Array()的原型是Array.prototype
  • let square = new Square()的原型是Square.prototype
  • let fn = new Function()的原型是Function.prototype

因为new的骚操作是故意这么做的

new X()自动做了四件事

自动创建空对象

自动为空对象关联原型,原型地址为X.prototype

X.prototype就存着一个地址(比如#309),把这个地址(#309)放到对象的x.__proto__

结论

你是谁构造的,你的原型就是谁的prototype属性对应的对象

prototype存的地址对应内存中的原型的对象

说‘prototype就是原型’并不严谨,prototype只是存了原型的地址

原型公式

  • 对象.__proto__ === 其构造函数.prototype

    1
    2
    
    window.Object.__proto__ === window.Function.prototype //true
    window.Object.__proto__ === Function.prototype //true
    

JS 中 proto 和 prototype 存在的意义是什么? by 方应杭

必须看文档,来确定某个特定的构造函数,传入不同个数的形参分别代表什么,以及来确定可以接受几个参数

1
2
let arr1 = new Array(3) // new Array(array.length)
let arr2 = new Array(3,5) // new Array(element0,element1[,...[,elementN]])

测试题

难度1

let x = {} 问: 1.x的原型是什么? Object.prototype(对应的对象) 2.x.__proto__的值是什么? 3.上面两个问题是等价的吗? 是等价的: x.__proto__ === Object.prototype 4.用内存图画出x的所有属性

let x = {}x:#909

#909:{__proto__:#309}

window.Object:#101

#101:{prototype:#309,...}

#309:{toString,...}

难度2

let square = new Square(5) 问 1.square的原型是什么? 原型是Square.prototype 2.square.__proto__的值是什么?(等价于上一问) 3.用内存图画出x的所有属性

难度3

问 1.Object.prototype是哪个函数构造出来的? 没有,不存在这个函数 2.Object.prototype的原型是什么? 无,无父无母 3.Object.prototype.__proto__null 4.用内存图画出来

1
2
3
var x = Object.prototype
x.__proto__ === Object.prototype //false
x.__proto__ //null

通过Square理解构造函数、prototypenew再看

Square最终版( ?)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 function Square(width){
     this.width = width
 }
 Square.prototype.getArea = function (){
     return this.width * this.width
 }
 Square.prototype.getLength = function(){
     return this.width * 4
 }
 let square = new Square(5)
 square.width
 square.getArea()
 square.getLength()

圆形

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function Circle(radius){
    this.radius = radius
}
Circle.prototype.getLength = function(){
    return this.radius * 2 * Math.PI
}
Circle.prototype.getArea = function(){
   /* return this.radius ** 2 * Math.PI */
   return Math.pow(this.radius,2) * Math.PI
}
let c1 = new Circle(10)
c1.radius //10
c1.getLength()
c1.getArea()
  • 要用this就不能用箭头函数

长方形

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function Rect(width,height){
    this.width = width
    this.height = height
}
Rect.prototype.getArea = function(){
    return this.width * this.height
}
Rect.prototype.getAreaLength = function(){
    return (this.width + this.height) * 2
}
let rect1 = new Rect(4,5)
rect1.width
rect1.height
rect1.getArea()
rect1.getLength()

再次问:对象需要分类吗

对象需要分类的理由

理由一

  • 有很多对象拥有一样的属性和行为
  • 需要把它们分为同一类,如square1square2
  • 这样创建类似对象的时候就很方便

理由二

  • 但还是有很多对象拥有其他的属性和行为
  • 必须有不同的分类,比如Square/Circle/Rect就是不同的分类
  • Array/Function也是不同的分类
  • Object创建出来的对象,是最没有特点的对象

类型 V.S. 类

类型

类型是JS数据的分类,目前有8种 五基两空以对象

类是针对于对象的分类,可以有无数种 常见的有ArrayFunctionDateRegExp

数组对象

定义一个数组对象

1
2
3
let arr = [1,2,3]  //字符字面量
let arr = new Array(1,2,3) //数组元素为1,2,3
let arr = new Array(3) //空数组的长度为3

数组对象的自身属性

  • '0'/'1'/'2'…下标属性和'length'
  • 注意,属性名没有数字,只有字符串

    1
    2
    
    let arr1 = new Array(1,2,3)
    Object.keys(arr1) //["1","2","3"],length属性遍历不到
    

数组对象的共用属性

  • 'push'/'pop'/'shift'/'unshift'/'join'
  • 注意,属性名没有数字,只有字符串
  • JavaScript 标准内置对象Array

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    let arr1 = new Array(1,2,3)
    arr1.push(4) //4
    arr1 //[1,2,3,4]
    arr1.pop // 4
    arr1 //[1,2,3]
    arr1.shift // 1 提档、上档,即删除第一个数组元素
    arr1 //[2,3]
    arr1.unshift(6,5) //2 返回数组新长度length
    arr2 = [1,2,3,4]
    arr2.join('-') //"1-2-3-4"
    arr3 = [3]
    arr4 = [4]
    arr3.concat(arr4) //[3,4]
    

数组对象比Object对象多一层共有属性

优先用结构上距离近的的同名属性

函数对象

定义一个函数

  • function fn(x,y){return x+y}
  • let fn2 = function fn(x,y){return x+y}
  • let fn = (x,y) => x+y
  • let fn = new Function('x','y','return x + y')

函数对象自身属性

  • 'name'
  • 'length'

    1
    2
    3
    4
    
    let f = new Function('x','y','return x+y')
    f.name //"anonymous"
    f.length //2
    console.dir(f)
    

函数对象共有属性

  • 'call'
  • 'apply'
  • 'bind'
  • 其他

JS终极一问

window是谁构造的

  • Window
  • 可以通过window.constructor属性看出构造者
  • window.__proto__ === Window.prototype,返回true

window.Object是谁构造的

  • window.Function
  • window.Object.constructor,返回Function(){..}
  • window.Object.constructor === window.Function,返回true
  • window.Object.__proto__ === window.Function.prototype
  • 因为所有函数都是window.Function构造的

window.Function是谁构造的

  • window.Function
  • window.Function.constructor
  • window.Function.constructor === window.Function
  • 因为所有函数都是window.Function构造的
  • 并不是自己构造自己,浏览器的JS引擎构造了Function,然后指定它的构造者是自己

这个代码被某些前端是认为过时的

1
2
3
4
5
6
7
8
9
function Square(width){
    this.width = width
}
Square.prototype.getArea = function(){
    return this.width * this.width
}
Square.prototype.getLength = function(){
    return this.width * 4
}

JS prototype V.S. JS class

你可以不会 class,但是一定要学会 prototype


ES6引入的类的语法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Square{
    constructor(width){ //构造函数
        this.width = width
    }
    getArea(){ //共有属性 最潮写法
        return this.width * this.width
    }
    getLength = function(){ //共有属性 复古写法
        return this.width * 4
    }//函数的两种写法都对
}

开启class时代

class语法引入更多概念

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Square{
    static x = 1
    width = 0 // 初始化
    constructor(width){
        this.width = width
    }
    getArea(){
        return this.width * this.width
    }
    getLength = function(){
        return this.width * 4
    }
    get area2(){ //只读属性 get,调用时只需Square.area2,不用加`()`
        return this.width * this.width
    }
}

学习语法的建议,在实践中学,查类的MDN文档类的定义类表达式

整理的 ES6 所有新特性

MDN的 ES6 关于类的语法1

MDN的 ES6 关于类的语法2

MDN的 ES6 关于类的语法3

class重写Circle

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Circle{
    constructor(radius){
        this.radius = radius
    }
    getArea(){
        reutrn Math.pow(this.radius,2) * Math.PI
    }
    getLength(){
        reutrn this.radius * 2 * Math.PI
    }
    let circle = new Circle(5)
    circle.radius
    circle.getArea()
    circle.getLength()
}

class重写Rect

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Rect{
    constructor(width,height){
        this.width = width
        this.height = height
    }
    getArea(){
        return this.width * this.height
    }
    getLength(){
        return (this.width + this.height) * 2
    }
    let rect = new Rect(4,5)
    rect.width
    rect.height
    rect.getArea()
    rect.getLength()
}

原型思路清晰简单,还是类容易上手?都是给对象分类的,想想不同风格之间的差别,面试时可以扯


·未完待续·

参考文章

JS 对象分类.pdf

你可以不会 class,但是一定要学会 prototype by 方应杭

JS 的 new 到底是干什么的? by 方应杭

JS 中 proto 和 prototype 存在的意义是什么? by 方应杭

饥人谷整理的 ES6 所有新特性

MDN的 ES6 关于类的语法1

MDN的 ES6 关于类的语法2

MDN的 ES6 关于类的语法3

相关文章