Hallo, mein Name ist Dmitry Karlovsky und (solange ich mich erinnern kann) kämpfe ich mit meiner Umgebung. Immerhin ist es so Knochen, Eiche, und es versteht nie, was ich davon will. Aber irgendwann wurde mir klar, dass es genug war, um es zu ertragen, und dass etwas geändert werden musste. Daher ist es jetzt nicht die Umgebung, die mir vorschreibt, was ich kann und was nicht, sondern ich diktiere der Umgebung, was es sein soll.
Wie Sie bereits verstanden haben, werden wir weiter über die Umkehrung der Kontrolle durch den "Umgebungskontext" sprechen. Viele Menschen kennen diesen Ansatz bereits aus den "Umgebungsvariablen" - sie werden beim Start des Programms festgelegt und normalerweise für alle Programme vererbt, die es startet. Wir werden dieses Konzept verwenden, um unseren TypeScript-Code zu organisieren.
Also, was wir bekommen wollen:
Funktionen erben beim Aufruf den Kontext der aufrufenden Funktion.
Objekte erben den Kontext von ihrem Eigentümerobjekt
Ein System kann gleichzeitig viele Kontextoptionen haben
Änderungen in abgeleiteten Kontexten wirken sich nicht auf das Original aus
Änderungen im ursprünglichen Kontext spiegeln sich in Derivaten wider
Tests können in isoliertem und nicht isoliertem Kontext ausgeführt werden
Boilerplate Minimum
Maximale Performance
Der Typcheck von allem
, - :
namespace $ {
export let $user_name: string = 'Anonymous'
}
- . , :
namespace $ {
export function $log( this: $, ... params: unknown[] ) {
console.log( ... params )
}
}
this
. , :
$log( 123 ) // Error
- . , :
$.$log( 123 ) // OK
, $
- , . :
namespace $ {
export type $ = typeof $
}
this
, . , , :
namespace $ {
export function $hello( this: $ ) {
this.$log( 'Hello ' + this.$user_name )
}
}
. , . , , , :
namespace $ {
export function $ambient(
this: $,
over = {} as Partial< $ >,
): $ {
const context = Object.create( this )
for( const field of Object.getOwnPropertyNames( over ) ) {
const descr = Object.getOwnPropertyDescriptor( over, field )!
Object.defineProperty( context, field, descr )
}
return context
}
}
Object.create
, , . Object.assign
, , , . , :
namespace $.test {
export function $hello_greets_anon_by_default( this: $ ) {
const logs = [] as unknown[]
this.$log = logs.push.bind( logs )
this.$hello()
this.$assert( logs, [ 'Hello Anonymous' ] )
}
}
, - $log
, . , , , , . :
namespace $ {
export function $assert< Value >( a: Value, b: Value ) {
const sa = JSON.stringify( a, null, '\t' )
const sb = JSON.stringify( b, null, '\t' )
if( sa === sb ) return
throw new Error( `Not equal\n${sa}\n${sb}`)
}
}
, $.$test
. , :
namespace $ {
export async function $test_run( this: $ ) {
for( const test of Object.values( this.$test ) ) {
await test.call( this.$isolated() )
}
this.$log( 'All tests passed' )
}
}
, . , , ( , , , , ..). , :
namespace $ {
export function $isolated( this: $ ) {
return this.$ambient({})
}
}
$log
, - . , $isolated
, $log
:
namespace $ {
const base = $isolated
$.$isolated = function( this: $ ) {
return base.call( this ).$ambient({
$log: ()=> {}
})
}
}
, $log
.
, :
namespace $.test {
export function $hello_greets_overrided_name( this: $ ) {
const logs = [] as unknown[]
this.$log = logs.push.bind( logs )
const context = this.$ambient({ $user_name: 'Jin' })
context.$hello()
this.$hello()
this.$assert( logs, [ 'Hello Jin', 'Hello Anonymous' ] )
}
}
. :
namespace $ {
export class $thing {
constructor( private _$: $ ) {}
get $() { return this._$ }
}
}
. , . , . , , , :
namespace $ {
export class $hello_card extends $thing {
get $() {
return super.$.$ambient({
$user_name: super.$.$user_name + '!'
})
}
get user_name() {
return this.$.$user_name
}
set user_name( next: string ) {
this.$.$user_name = next
}
run() {
this.$.$hello()
}
}
}
, , :
namespace $.test {
export function $hello_card_greets_anon_with_suffix( this: $ ) {
const logs = [] as unknown[]
this.$log = logs.push.bind( logs )
const card = new $hello_card( this )
card.run()
this.$assert( logs, [ 'Hello Anonymous!' ] )
}
}
, , . , , . , :
namespace $ {
export class $hello_page extends $thing {
get $() {
return super.$.$ambient({
$user_name: 'Jin'
})
}
@ $mem
get Card() {
return new this.$.$hello_card( this.$ )
}
get user_name() {
return this.Card.user_name
}
set user_name( next: string ) {
this.Card.user_name = next
}
run() {
this.Card.run()
}
}
}
. . $mem
. :
namespace $ {
export function $mem(
host: object,
field: string,
descr: PropertyDescriptor,
) {
const store = new WeakMap< object, any >()
return {
... descr,
get() {
let val = store.get( this )
if( val !== undefined ) return val
val = descr.get!.call( this )
store.set( this, val )
return val
}
}
}
}
WeakMap
, . , , , :
namespace $.test {
export function $hello_page_greets_overrided_name_with_suffix( this: $ ) {
const logs = [] as unknown[]
this.$log = logs.push.bind( logs )
const page = new $hello_page( this )
page.run()
this.$assert( logs, [ 'Hello Jin!' ] )
}
}
, . - . , , .
namespace $ {
export class $app_card extends $.$hello_card {
get $() {
const form = this
return super.$.$ambient({
get $user_name() { return form.user_name },
set $user_name( next: string ) { form.user_name = next }
})
}
get user_name() {
return super.$.$storage_local.getItem( 'user_name' ) ?? super.$.$user_name
}
set user_name( next: string ) {
super.$.$storage_local.setItem( 'user_name', next )
}
}
}
- :
namespace $ {
export const $storage_local: Storage = window.localStorage
}
, , , :
namespace $ {
const base = $isolated
$.$isolated = function( this: $ ) {
const state = new Map< string, string >()
return base.call( this ).$ambient({
$storage_local: {
getItem( key: string ){ return state.get( key ) ?? null },
setItem( key: string, val: string ) { state.set( key, val ) },
removeItem( key: string ) { state.delete( key ) },
key( index: number ) { return [ ... state.keys() ][ index ] ?? null },
get length() { return state.size },
clear() { state.clear() },
}
})
}
}
, , , $hello_card
$app_card
, .
namespace $ {
export class $app extends $thing {
get $() {
return super.$.$ambient({
$hello_card: $app_card,
})
}
@ $mem
get Hello() {
return new this.$.$hello_page( this.$ )
}
get user_name() {
return this.Hello.user_name
}
rename() {
this.Hello.user_name = 'John'
}
}
}
, , , , , , , , :
namespace $.$test {
export function $changable_user_name_in_object_tree( this: $ ) {
const name_old = this.$storage_local.getItem( 'user_name' )
this.$storage_local.removeItem( 'user_name' )
const app1 = new $app( this )
this.$assert( app1.user_name, 'Jin!' )
app1.rename()
this.$assert( app1.user_name, 'John' )
const app2 = new $app( this )
this.$assert( app2.user_name, 'John' )
this.$storage_local.removeItem( 'user_name' )
this.$assert( app2.user_name, 'Jin!' )
if( name_old !== null ) {
this.$storage_local.setItem( 'user_name', name_old )
}
}
}
, , . .
, , :
namespace $ {
$.$test_run()
}
, , , . , $isolated
, - :
namespace $ {
$.$ambient({
$isolated: $.$ambient
}).$test_run()
}
, , , localStorage, $storage_local
.
, , , , .
$mol, . …
c import/export, : Fully Qualified Names vs Imports. , : PascalCase vs camelCase vs kebab case vs snake_case.