Safe-guarding AngularJS scopes with ECMAScript 5 "Strict Mode"
Having a history as a Java developer I prefer declaring the complete JavaScript object at once through an object literal; in a similar fashion as your would declare a class in Java. In my opinion adding new properties "on the fly" to a JavaScript object is a very bad practice:
var jsLibrary = { name: "AngularJS" };
// adds a new property "homepage" to the existing object...
jsLibrary.homepage = "http://www.angularjs.org/";
For the same reasons I dislike how properties are declared in an AngularJS application:
$scope._<property-name>_ = _..._;
Declaring scopes as object literals
In order to use an object literal to declare your scope one could use the following syntax:
$scope = angular.extend($scope, {
_propertyName_: _value_,
_propertyName_: _value_,
});
To improve the construction above we could (instead of using angular.extend
)
add a declare function to all scope implementations. This can be achieved easily
by adding such a function to the $rootScope
:
angular.module("blog.jdriven", []).run(function ($rootScope) {
$rootScope.declare = function (obj) {
return angular.extend(this, obj);
};
});
Now we can rewrite our scope declaration as follows:
$scope = $scope.declare({
_propertyName_: _value_,
_propertyName_: _value_,
});
Keeping this
in scope Using an object literal we can now use this
to
refer the scope instance from inside a function declared in the object literal:
```javascript $scope = $scope.declare({ // ... todoText: "",
addTodo: function () { // ... this.todoText = ""; }, // ... }); ```
The scope declared using the code above contains:
- a property
todoText
which is initialized to an empty string - a function
addTodo
that will reset thetodoText
property to its default value
Now have a look at the following dump of the internal contents of our scope:
In this screenshot (from the Chrome browser) you will notice that:
- Our scope does indeed contain the
todoText
property and theaddTodo
function (as well as some other properties / functions omitted from our code sample) - The scope (at this specific moment) is a top-level scope, meaning it isn't contained by any parent scope
- The scope is a direct descendant from the
$rootScope
(which always has an$id
of "002")
But what if our scope would actually contain a nested TheChildCtrl
scope like
this:
<div ng-controller="TodoCtrl">
...
<div ng-controller="TheChildCtrl">
<form ng-submit="addTodo()"></form>
</div>
</div>
This extra TheChildCtrl
scope would actually introduce an issue in our
addTodo
function. The this
, used in the this.todoText = '';
statement, no
longer would refer to the TodoCtrl
scope but instead to the TheChildCtrl
scope. To illustrate this have look at following dump to see what the
TheChildCtrl
scope and its parent (= TodoCtrl
) scope will look like after
the "addTodo()" function was invoked:
The invocation of the addTodo
function accidentally introduced a new
todoText
property in the TheChildCtrl
scope. To prevent this accidental
introduction of properties we will modify our declare
function to:
- manually copy the properties from the object literal (instead of using
angular.extend
) - explicitly bind each function to the scope instance (using the
angular.bind
function) to enforce thethis
of each scope function:
// ...
$rootScope.declare = function (obj) {
var self = this;
angular.forEach(obj, function (value, key) {
self[key] = angular.isFunction(value) ? angular.bind(self, value) : value;
});
return this;
};
// ...
Now that the "this" in back 'in scope' our "declare" function is fully
functional. To illustrate the benefit of the declare
scope syntax I've
rewritten the "Todo" sample from the AngularJS homepage in
a before and
after jsFiddle. Safe-guarding your scopes In
order to ensure that no other properties can be assigned to our $scope
than
the properties of our object literal we can enhance our declare
function to
returned a "sealed" object instance through return Object.seal(this);
. The
Object.seal
function is part of EcmaScript 5 and will prevent future
extensions to an object and protects existing properties from being deleted.
However sealing and object isn't enough since by default no errors will be
thrown or logged when the seal is violated. To make object sealing useful we
need to enable the "strict mode" from EcmaScript 5. Enabling it will cause
TypeError's to be thrown when the seal is violated. All modern browser except
Internet Explorer (< IE10) support the usage of "strict mode" To enable strict
mode one needs to add the following 'magic' in front of every ".js" file:
"use strict";
Using object sealing combined with the ECMAScript 5 "strict mode" we can now make a much more advanced implementation of the "declare" function that:
- enforces the actual usage of
$scope.declare
; the $scope instance supplied to the controller is of the special extension of the controller $scope of type "UndeclaredScope" which is sealed and only allows invocation of it'sdeclare
function. - The
declare
function works in similar fashion as before but now it will also check if the browser actually supports "strict mode"; if this isn't the case (like in Internet Explorer 9 or earlier) it will instead execute to the earlier described "basic" implementation of "declare".
A complete "sealed" + "strict mode" implementation can be found in this jsFiddle.
It includes some commented-out code that all would raise a "TypeError" in case executed inside a "strict mode" browser.