└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # ECMAScript Proposal: Syntax for Explicitly Naming `this` 2 | 3 | > `this` isn't really a keyword, it is the natural "main" function argument in JavaScript. 4 | > - (@spion)[https://github.com/mindeavor/es-pipeline-operator/issues/2#issuecomment-162348536] 5 | 6 | This proposal extends the `function` declaration syntax to allow for explicit naming of what is normally called `this`. 7 | 8 | ```js 9 | Object.defineProperty(User.prototype, { 10 | get: function fullName() { // original version 11 | return `${this.firstName} ${this.lastName}` 12 | }, 13 | configurable: true, 14 | }) 15 | 16 | // versions use the feature of this proposal 17 | 18 | function fullName(this) { // explicit this parameter 19 | return `${this.firstName} ${this.lastName}` 20 | } 21 | 22 | function fullName(this user) { // explicit naming `this` to `user` 23 | return `${user.firstName} ${user.lastName}` 24 | } 25 | 26 | function fullName(this {firstName, lastName}) { // destructuring 27 | return `${firstName} ${lastName}` 28 | } 29 | ``` 30 | 31 | Its primary use case is to play nicely with the [function bind proposal](https://github.com/zenparsing/es-function-bind), making such functions more readable. For example: 32 | 33 | ```js 34 | function zip (this array, otherArray) { 35 | return array.map( (a, i) => [a, otherArray[i]] ) 36 | } 37 | 38 | function flatten (this subject) { 39 | return subject.reduce( (a,b) => a.concat(b) ) 40 | } 41 | 42 | [10,20] 43 | ::zip( [1,2] ) 44 | ::flatten() 45 | //=> [10, 1, 20, 2] 46 | ``` 47 | 48 | ## Motivation 49 | 50 | The "keyword" `this` has been one of the greatest sources of confusion for programmers learning and using JavaScript. Fundamentally, it boils down to **two primary reasons**: 51 | 52 | 1. `this` is an **implicit** parameter, and 53 | 2. `this` **implicitly performs variable shadowing** in your functions. 54 | 55 | Variable shadowing is true source of why `this` is so confusing to learn. In fact, variable shadowing is a bad practice in general. For example: 56 | 57 | ```js 58 | function process (obj, name) { 59 | obj.taskName = name; 60 | doAsync(function (obj, amount) { 61 | obj.x += amount; 62 | }); 63 | }; 64 | ``` 65 | 66 | In the above code, `obj.x` is referring to a different `obj` than `obj.name`. An experienced programmer would likely think this code is silly. Why name the inner parameter `obj` when there is already another variable in the same scope with the same name? 67 | 68 | Yet, this behavior is exactly what `this` proceeds to do. If we were to translate the above example to use method-style functions: 69 | 70 | ```js 71 | function process (name) { 72 | this.taskName = name; 73 | doAsync(function (amount) { 74 | this.x += amount; 75 | }); 76 | }; 77 | ``` 78 | 79 | ...we would end up with code *just as bad* as the original example. The second `this` is referring to a different object than the first `this`, even though they have the same name. 80 | 81 | However, if we could **explicitly name** the object within the function parameters, we could disambiguate the two to avoid such variable shadowing, and make the above example look something more like this: 82 | 83 | ```js 84 | function process (this obj, name) { 85 | obj.taskName = name; 86 | doAsync(function callback (this result, amount) { 87 | result.amount += 2; 88 | }); 89 | }; 90 | ``` 91 | 92 | Explicit `this` parameter also allow type annotation or parameter decorators be added just like normal parameter. 93 | 94 | ```ts 95 | // Type annotation (TypeScript, already possible today) 96 | Number.prototype.toHexString = function (this: number) { 97 | return this.toString(16) 98 | } 99 | ``` 100 | 101 | ```ts 102 | // Parameter decorators (future proposal) 103 | Number.prototype.toHexString = function (@toNumber this num) { 104 | return num.toString(16) // same as Number(num).toString(16) 105 | } 106 | ``` 107 | 108 | ## Protect programmers by throwing as early as possible 109 | 110 | If a function explicitly names `this`, attempting to use `this` inside the body will throw an error: 111 | 112 | ```js 113 | function method (this elem, e) { 114 | console.log("Elem:", elem) // OK 115 | console.log("this:", this) // <-- Syntax error! 116 | } 117 | ``` 118 | 119 | This behavior fits well with arrow functions, since they don't contain their own `this` in the first place. 120 | 121 | ```js 122 | function callback (this elem, e) { 123 | alert(`You entered: ${elem.value}`); 124 | setTimeout( () => elem.value = '', 1000 ) // OK 125 | setTimeout( () => this.value = '', 1000 ) // <-- Syntax error! 126 | } 127 | ``` 128 | 129 | As normal, an inner `function` get its own `this`, which you can still choose whether or not to rename: 130 | 131 | ```js 132 | runTask(function cb (this elem, e) { 133 | 134 | elem.runAsyncTask(function () { 135 | console.log("Outer elem:", elem) // OK 136 | console.log("Inner this:", this) // OK 137 | }); 138 | }) 139 | ``` 140 | 141 | Class constructor can't use explicit `this` syntax because class constructor can only be used via `new` and `this` in the constructor is never a parameter or argument passed by caller, the `this` value in constructor is generated by the constructor or its base class. 142 | 143 | ```js 144 | class C { 145 | constructor(this) { // <-- Syntax error! 146 | // ... 147 | } 148 | } 149 | ``` 150 | 151 | If a function has explicitly named its `this` parameter, it could be useful to throw an error when that function gets called without one or used via `new`. For example: 152 | 153 | ```js 154 | function zip (this array, otherArray) { 155 | return array.map( (a, i) => [a, otherArray[i]] ) 156 | } 157 | 158 | zip() //=> Throws a TypeError on invocation, 159 | // before `array.map(...)` is run. 160 | 161 | new zip() //=> Also throws a TypeError 162 | ``` 163 | 164 | ## Alternate Syntax 165 | 166 | Matching the [function bind proposal](https://github.com/zenparsing/es-function-bind): 167 | 168 | ```js 169 | function array::zip (otherArray) { 170 | return array.map( (a, i) => [a, otherArray[i]] ) 171 | } 172 | 173 | function subject::flatten () { 174 | return subject.reduce( (a,b) => a.concat(b) ) 175 | } 176 | 177 | [10,20] 178 | ::zip( [1,2] ) 179 | ::flatten() 180 | //=> [10, 1, 20, 2] 181 | ``` 182 | --------------------------------------------------------------------------------