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, {
_<property-name>_: _..._,
_<property-name>_: _..._
});
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({
_<property-name>_: _..._,
_<property-name>_: _..._
});
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:
$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()">
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.