漫谈 JavaScript 里的对象、继承、作用域与其它

发布于 2022-03-11 19:51:57 字数 7888 浏览 1085 评论 0

工具:Babel 在线编译:https://babeljs.io/repl/

好用的对象字面量与进阶

「一切都是对象」是简洁概念,但是像下面这样创建实例太辛苦。

var person = new Object()
person.firstName = 'Jade'
person.lastName = 'Gu'
person.name = 'Jade Gu'

我们想要更直观的方式,如:

var person = {
    firstName: 'Jade',
    lastName: 'Gu',
    name: 'Jade Gu'
}

如你所见,name属性,无非是firstNamelastName的组合,而上面我却得重复两次,实在不便。

我们需要一个「计算属性」,在需要用到时,它自行根据已有属性计算出结果。

之前的 JavaScript 没有提供便捷的方法,我们得用下面这种冗长做法。

var person = {
    firstName: 'Jade',
    lastName: 'Gu'
}

Object.defineProperty(person, 'name', {
    get: function() {
        return [this.firstName, this.lastName].join(' ')
    }
})

person.name // Jade Gu

只是写个姓名而已,竟然难以找到编写的舒服姿势,很难受是吧?

ES2015 带了了一些福音。让我们可以这样写:

var person = {
    firstName: 'Jade',
    lastName: 'Gu',
    get name() {
        return [this.firstName, this.lastName].join(' ')
    }
}

经过 Babel 编译后是这个模样:

'use strict';

var person = Object.defineProperties({
    firstName: 'Jade',
    lastName: 'Gu'
}, {
    name: {
        get: function get() {
            return [this.firstName, this.lastName].join(' ');
        },
        configurable: true,
        enumerable: true
    }
});

如果只写一个人名,那么上面的足够了;但实际上我们需要用到很多人名,每次都写get name,心很累。

我们要封装,对于人名做「最小数据关注量」。比如下面:

function createPerson(firstName, lastName) {
    return {
        firstName: firstName,
        lastName: lastName,
        get name() {
            return [this.firstName, this.lastName].join(' ')
        }
    }
}

var person1 = createPerson('Jade', 'Gu')
var person2 = createPerson('Hehe', 'Da')

这个方式叫「工厂模式」。在这个场景中,它有两大不美。其一是createPerson名字冗长,其二是get name每次都会创建一个新的函数。

我们想要「最小打字量」跟「最小内存占用」。下面这种方式,更接近我们的目标:

function Person(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
}

Object.defineProperty(Person.prototype, 'name', {
    get: function() {
        return [this.firstName, this.lastName].join(' ')
    }   
})

var person1 = new Person('Jade', 'Gu')
var person2 = new Person('Hehe', 'Da')
person1.name // "Jade Gu"
person2.name // "Hehe Da"

原型上的计算属性,也能影响到实例。当实例自身没有name属性时,JS 引擎查找原型上有没有,原型有个同名计算属性,被查找时也就启动了计算,然后返回结果。计算 name 的函数只需要一份,放在原型对象上就可以了。省了内存。

这样我们用new取代了createnew Person而不是createPerson,省了创建时的打字量。

但如你所见,在定义时,不够美观,感觉给Person.prototype做特殊处理,而不是很自然的定义Person

name当然是Person很自然要拥有的属性之一,为什么要在定义时要显示地用别的函数(Object.defineProperty)?

所以,我们要用 ES2015 的语法

class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }
    get name() {
        return `${this.firstName} ${this.lastName}`
    }
}
const person1 = new Person('Jade', 'Gu')
const person2 = new Person('Hehe', 'Da')

这下感受自然得多。再来感受一下python的语法:

class Person(object):
    def __init__(self, firstName, lastName):
        self.firstName = firstName
        self.lastName = lastName
    @property
    def name(self):
        return '%s %s' % (self.firstName, self.lastName)

person1 = Person('Jade', 'Gu')
person2 = Person('Hehe', 'Da')

在 ES2015 出现之前,我觉得 python 的语法很干净。而现在,我更倾向于 ES2015。

在 python 中,只要用装饰符@property,就可以更自然的定义计算属性,将一个 name 方法调用当做属性来使用。

可惜在 ES2015 中,还没有装饰符。然而可期的是,ES2016 可能有。现在用 Babel 也可以书写了。

function readonly(target, name, descriptor) {
    return {
        get: descriptor.value
    }
}

class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }
    @readonly
    name() {
        return `${this.firstName} ${this.lastName}`
    }
}
const person1 = new Person('Jade', 'Gu')
const person2 = new Person('Hehe', 'Da')

好吧,看起来还不如get name。在目前这个简单场景内没有优势,但更复杂的情况下,装饰符能很好地分离复杂度,将那些固定的、与业务无关的特性处理工作,移出「定义点」,让「定义点」代码更干净与直观。

旨在共享数据的原型与作用域

如前一节所示,在 ES2015 之前,JavaScript 里定义最简单的人名也像「醉汉走路」。在七扭八歪的代码里,才终于达到了目标。

我们的目标是:更小打字量,更小内存消耗。

这意味着,我们要共享很多东西,才能抑制暴涨。

「继承与组合」是共享的两大方式。

共享的原理是:「世界终究是孙子们的」。不信,你看:

function grandpa() {
    var grandpa_money = 1000000

    function father() {
        var father_money = 500000

        function me() {
            var my_money = 300000

            function child() {
                var child_money = 100000

                function grandson() {
                    var grandson_money = 100
                    grandson_money += child_money
                    grandson_money += my_money
                    grandson_money += father_money
                    grandson_money += grandpa_money
                    child_money = my_money = father_money = grandpa_money = 0
                    child = me = father = grandpa = null
                    console.log(grandson_money)
                    console.log(child_money, my_money, father_money, grandpa_money)
                    console.log(child, me, father, grandpa)
                }
                grandson()
            }
            child()
        }
        me()

    }
    father()
}
grandpa()

祖父拥有1000000,创造了父亲;

父亲拥有500000,创造了我;

我拥有300000,创造了儿子;

儿子拥有100000,创造了孙子;

孙子拥有100,把祖上的前都败光,并且欺师灭祖。

这就是 JavaScript 里的作用域链;根据创建时间与环境,确定可支配变量的范围。

正如我们自诩拥有的「五千年文化遗产」一样,最后创建的,最里层的grandson可支配祖上所有变量。

这样设计的好处是,如果我需要的数据已经在内存中,那么就不必重复创建。然后两个平级的函数之间无法互相访问对方的私有变量,就做到了「数据隐私控制」。

上面是作用域链层面的数据共享,下面看看原型链层面的数据共享。

var Ancestors = {
    '人的成长': '生老病死',
    '地球位置': '世界中心',
    '地球形状': '天圆地方',
    '人类起源': '上帝造人'
}
var SomeoneA = Object.create(Ancestors)

Object.assign(SomeoneA, {
    '地球位置': '太阳系中心',
    '地球形状': '地球是圆的',
    '人类起源': '物种演化'
})

var SomeoneB = Object.create(SomeoneA)

Object.assign(SomeoneB, {
    '地球位置': 'https://zh.wikipedia.org/wiki/%E5%9C%B0%E7%90%83%E5%9C%A8%E5%AE%87%E5%AE%99%E4%B8%AD%E7%9A%84%E4%BD%8D%E7%BD%AE',
    '地球形状': 'https://www.google.com/search?q=%E5%9C%B0%E7%90%83%E5%BD%A2%E7%8A%B6&es_sm=122&tbm=isch&tbo=u&source=univ&sa=X&ved=0CB4QsARqFQoTCJv1iZvb4cYCFRIakgod5XAAyg&biw=1680&bih=912',
    '人类起源': '裸猿/走出非洲大草原'
})

原型的数据共享方式,类似于科学知识的发展。

你没有新发现,你所有知识都来自于前人的研究成果。

你有新发现,就以你的新发现为准。你推翻(删除)了你的新发现,还是以祖先的为准。

你访问SomeoneB能得到最新的数据,其中「人的成长」是在这里是颠扑不破的数据,还是沿用祖先的。

你要获取某一时期的数据,访问那个时期的对象即可。如此,获取数据以及数据之间的关系,就很便利了。

然而,知识可以共享与迭代,技能呢?

我如何在 JavaScript 中,写出简易的物种演化模型?

演化是一点点的积累,不是覆盖,不能每个物种都重新发明所有技能;所以,class就显得很重要

//只会说呵呵哒的「人」
class H {
    constructor() {
        this.type = 'h'
    }
    say() {
        console.log('呵呵哒')
    }
}

class Hu extends H {
    constructor() {
            super()
            this.type = 'hu' //覆盖你
        },
        think() {
            return Math.random() > 0.5 ? '么么哒' : null
        }
    say() {
        //有想法说想法,没想法呵呵哒
        if (!this.think) {
            super.say()
        } else {
            console.log(this.mind)
        }
    }
}

class Hum extends Hu {
    constructor() {
        super()
        this.type = 'hum'
        this.memory = {}
    }
    remember(information) {
        this.memory[new Date().getTime()] = information
    }
    think(key) {
        return key ? this.memory[key] : super.think()
    }
}

class Human extends Hum {
    constructor() {
        super()
        this.type = 'human'
    }
    think(key) {
        var result = super.think(key)
        return result.includes('Miss Right') ? 'I Love You' : result
    }
}

技能的传承跟数据的传承,性质不同。

我们可能不再需要祖先的错误知识,但还是无法离开祖先遗传下来的呼吸能力。

结语

嗯,这就是我眼中的 Javascript ,一副醉汉模样,好在有 Babel ,终于快清醒过来了。

这一醉,就是20年。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84960 人气
更多

推荐作者

linfzu01

文章 0 评论 0

可遇━不可求

文章 0 评论 0

枕梦

文章 0 评论 0

qq_3LFa8Q

文章 0 评论 0

JP

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文